The Observations type turns an @Observable model into an AsyncSequence, so you consume
changes with for await instead of Combine publishers or manual observation tracking.
Why withObservationTracking is not enough
withObservationTracking fires onChange once, for the first change only, so streaming
requires re-registering recursively. It also cannot feed an asynchronous for-loop.
func startObservation() {
withObservationTracking {
render(store.state)
} onChange: {
Task { startObservation() } // manual re-registration, every change
}
}
Observations: touch properties, get a stream
The closure you pass to Observations implicitly observes every observable property it reads.
Each change emits the closure's fresh return value into the async sequence.
let store = Store() // @Observable
struct State {
let items: [String]
let isLoading: Bool
}
let streamOfStates = Observations {
State(items: store.items, isLoading: store.isLoading)
}
for await state in streamOfStates {
render(state)
}
Updates are transactional: when items and isLoading change together, the stream emits one
combined value, not one per property.
A KeyPath convenience
A small extension gives any observable a per-property stream.
extension Observable {
func stream<Value: Sendable>(
of keyPath: KeyPath<Self, Value>
) -> any AsyncSequence<Value, Never> {
Observations { self[keyPath: keyPath] }
}
}
for await items in store.stream(of: \.items) {
print(items)
}