Create an AsyncStream from withObservationTracking() function
After Natalia wrote her previous post on how to implement the observer pattern in Swift in iOS 17 Using Observation framework outside of SwiftUI, I've been wondering if it would be possible to wrap observation into an AsyncStream
. That would let me use an asynchronous for
loop to iterate over changes. In this post I will share how I implemented it.
I used the same User
class from Natalia's post marked with @Observable
macro.
@Observable class User {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
Then I defined a function that returns an AsyncStream. The stream is created from the results of the apply
closure of withObservationTracking() function.
func observationTrackingStream<T>(
_ apply: @escaping () -> T
) -> AsyncStream<T> {
AsyncStream { continuation in
@Sendable func observe() {
let result = withObservationTracking {
apply()
} onChange: {
DispatchQueue.main.async {
observe()
}
}
continuation.yield(result)
}
observe()
}
}
I wanted the first iteration of the loop to include the current value, so I called the yield()
method on the result returned from withObservationTracking()
. When the observed values change, I schedule a new observe()
function call that will read the value and yield it to the AsyncStream
.
Note that since the onChange
callback of withObservationTracking()
is called before the property changes on the thread that is making the change, I use the async dispatch to ensure that we read the value after the property has changed. However, if changes are being made to these properties from other dispatch queues, I would need to adjust my code here to ensure that the observe()
function is called after the property value changes.
To use my observationTrackingStream()
function, I can create a new stream and then iterate over it.
let user = User(name: "Jane", age: 21)
let changes = observationTrackingStream {
user.age
}
for await age in changes {
print("User's age is \(age)")
}
This can be helpful in many places where we need to track changes to an observable class, such as trigging UI updates in a UIKit view controller, for example.