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.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35