WWDC24 deal: 30% off our Swift and SwiftUI books! Learn more ...WWDC24 deal:30% off our Swift and SwiftUI books >>

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.

Books by Natalia PanferovaBooks by Natalia Panferova
WWDC24: 30% off all books!
  • Swift Gems

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

  • Integrating SwiftUI into UIKit Apps

    A detailed guide on gradually adopting SwiftUI in UIKit projects

The offer is active until the 16th of June.