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.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Check out our book!

Integrating SwiftUI into UIKit Apps

Integrating SwiftUI intoUIKit Apps

UPDATED FOR iOS 17!

A detailed guide on gradually adopting SwiftUI in UIKit projects.

  • Discover various ways to add SwiftUI views to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit