Create an AsyncStream from withObservationTracking() function

Published by malhal on

I needed to create an AsyncStream to monitor changes to an @Observable model class’s var and by using withCheckedContinuation and AsyncStream‘s unfolding init I came up with this:

let modelIdentifiersDidChange = AsyncStream {
    await withCheckedContinuation { continuation in
        withObservationTracking {
            let _ = model.identifiers // let _ required to fix compilation error.
        } onChange: {
            continuation.resume() // model.identifiers doesn't contain the new value here yet.
        }
    }
    return model.identifiers // optionally insert the value into the stream
}

The challenge here is withObservationTracking only watches for a single change but we can wrap that in withCheckedContinuation which also is designed to only work once, but with AsyncStream’s nifty unfolding init it can monitor for multiple changes. The only ugly thing is the let _ = is required to fix a compilation error.

In case you were interested, here is the model:

@Observable
class Model {
    var identifiers = Set<String>()
}

Before I came up with my own solution I searched and found this one, however I didn’t like the DispatchQueue hack and I think AsyncStream’s unfolding init is a better option.

It can be a little tricky to work with a stream when you require the initial value and support cancellation and deal with the optionality of stream’s next function. I’ve been experimenting with the pattern below, where I don’t actually return the value from the stream but just use it as a notification that there has been a change and use a repeat/do while loop:

let modelIdentifiersDidChange = AsyncStream {
    await withCheckedContinuation { continuation in
        withObservationTracking { 
            let _ = model.identifiers // let _ required to fix compilation error.
        } onChange: {
            continuation.resume() // model.identifiers doesn't contain the new value here yet.
        }
    }
}
var iterator = modelIdentifiersDidChange.makeAsyncIterator()
repeat
{
    let identifiers = model.identifiers
     
    // do actual work
    print("task \(identifiers)")
    
}
while await iterator.next() != nil