Swift macros that automatically generate reactive observation patterns using Swift's Observation framework. Provides both basic observation tracking and advanced cancellable observation management.
ObservationTracking offers two macros:
@ObservationTracking
: Generates reactive observation for property assignments@CancellableObservation
: Adds advanced observation lifecycle management with cancellation, and selective control
Both macros work together to create robust observation patterns with minimal boilerplate code.
- Swift 6.0+
- Xcode 16.0+
- iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+
Add ObservationTracking to your project through Xcode or by adding it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/VAndrJ/ObservationTracking.git", from: "1.0.0")
]
Or add it through Xcode:
- File → Add Package Dependencies
- Enter the repository URL:
https://github.com/VAndrJ/ObservationTracking.git
- Select the version range
The @ObservationTracking
macro automatically generates reactive observation for each property assignment in a function:
import ObservationTracking
import Observation
@Observable
class DataModel {
var count = 0
var name = "Hello"
var isActive = false
}
class ViewController {
@ObservationTracking
private func bind() {
label.text = model.name
countLabel.text = "\(model.count)"
switchControl.isOn = model.isActive
}
}
The @ObservationTracking
macro supports an isolation
parameter that controls how observation change handlers are generated
.mainActor
(default): Executes change handlers inTask { @MainActor in ... }
..actor
: Executes change handlers inTask { await ... }
..none
: Executes change handlers directly without Task wrapping.
For a real-use case, see ExampleView
in the Example
project.
The @CancellableObservation
macro adds advanced observation management to classes:
@CancellableObservation
@MainActor
class WeatherObserver {
weak var model: WeatherModel?
var temperature = 0.0
var humidity = 0.0
@ObservationTracking
func bind() {
temperature = model?.temperature ?? 0.0
humidity = model?.humidity ?? 0.0
}
// Advanced control methods are automatically generated:
// - stopObservations()
// - startObservationsIfNeeded()
// - observeTemperature()
// - observeHumidity()
// - cancelObserveTemperature()
// - cancelObserveHumidity()
}
Control all observations at once:
// Stop all observations
observer.stopObservations()
// Restart all observations (automatically calls all @ObservationTracking functions)
observer.startObservationsIfNeeded()
The macro automatically handles cancellation with tokens (randomly generated string within the one observation update cycle).
class ManualObserver {
weak var store: SomeStore?
var localValue1 = 0
var localValue2 = ""
func startObserving() {
observeValue1()
observeValue2()
}
private func observeValue1() {
localValue1 = withObservationTracking {
store?.value1 ?? 0
} onChange: { [weak self] in
Task { @MainActor in
self?.observeValue1()
}
}
}
private func observeValue2() {
localValue2 = withObservationTracking {
store?.value2 ?? ""
} onChange: { [weak self] in
Task { @MainActor in
self?.observeValue2()
}
}
}
}
class MacroObserver {
weak var store: SomeStore?
var localValue1 = 0
var localValue2 = ""
@ObservationTracking
func startObserving() {
localValue1 = store?.value1 ?? 0
localValue2 = store?.value2 ?? ""
}
}
- Reduced Boilerplate: Eliminates repetitive
withObservationTracking
code - Type Safe: Compile-time macro expansion ensures type safety
- Clean Syntax: Makes intent clear and code more readable
- UIKit Friendly: Perfect for updating UI elements when data changes
- Advanced Control: Selective observation management with cancellation
- Macro Dependency: Requires understanding of Swift macros
Your code:
@ObservationTracking
private func bind() {
count = model.value
name = model.name ?? "default"
}
Generated code:
private func bind() {
observeCount()
observeName()
}
private func observeCount() {
count = withObservationTracking {
model.value
} onChange: { [weak self] in
Task { @MainActor in
self?.observeCount()
}
}
}
private func observeName() {
name = withObservationTracking {
model.name ?? "default"
} onChange: { [weak self] in
Task { @MainActor in
self?.observeName()
}
}
}
With .actor
isolation:
@ObservationTracking(isolation: .actor)
private func processData() {
result = model.computation
}
Generated code:
private func processData() {
observeResult()
}
private func observeResult() {
result = withObservationTracking {
model.computation
} onChange: { [weak self] in
Task {
await self?.observeResult()
}
}
}
With .none
isolation:
@ObservationTracking(isolation: .none)
private func updateCache() {
cached = model.data
}
Generated code:
private func updateCache() {
observeCached()
}
private func observeCached() {
cached = withObservationTracking {
model.data
} onChange: { [weak self] in
self?.observeCached()
}
}
Your code:
@CancellableObservation
class Observer {
@ObservationTracking
func bind() {
value = model.property
}
}
Generated infrastructure:
class Observer {
func bind() {
observeValue()
}
func observeValue() {
guard isObservingEnabled else { return }
let token = UUID().uuidString
observationTokens["observeValue"] = token
value = withObservationTracking {
model.property
} onChange: { [weak self] in
Task { @MainActor in
guard let self, token == self.observationTokens["observeValue"] else {
return
}
self.observeValue()
}
}
}
func cancelObserveValue() {
observationTokens.removeValue(forKey: "observeValue")
}
// Infrastructure
private var observationTokens: [String: String] = [:]
private var isObservingEnabled = true
func stopObservations() {
isObservingEnabled = false
observationTokens.removeAll()
}
func startObservationsIfNeeded() {
guard !isObservingEnabled else { return }
isObservingEnabled = true
bind() // Automatically calls all @ObservationTracking functions
}
}
- Property Detection: The macro scans function bodies for property assignments (
property = expression
) - Method Generation: Creates individual observer methods for each assignment
- Reactive Wrapping: Wraps right-hand side expressions with
withObservationTracking
- Auto Re-observation: Sets up
onChange
callbacks that re-execute the observer methods - Function Transformation: Replaces assignments in the original function with calls to observer methods
- Cancellation Management: Adds token-based cancellation and control infrastructure
- Automatic Restart:
startObservationsIfNeeded()
automatically calls all@ObservationTracking
functions
This project is licensed under the MIT License - see the LICENSE file for details.
Inspired by the need for cleaner observation code in UIKit.