How to use Observation to actually observe changes to a property

The summary is that the current implementation of Observable was pretty much tailor made for SwiftUI (so much so that I wonder if even withObservationTracking(_:onChange:) should have been under SPI...) Anyway, the plan is to enhance it for more general use-cases – but that hasn't happened yet.

For me, the crux of it is that asynchronous observation (as the removed values(for:) API supplied) provides very different functionality to a synchronous API (that can support fine-grained willSet/didSet events) and has the potential to introduce (and almost encourages) all sorts of subtle bugs unless handled with the utmost care.

This is because all these separate observations are carried by separate asynchronous Tasks which provide no guarantees as to the order in which they're delivered – other than that they won't be delivered immediately. Essentially you're dealing with a bunch of unordered state arriving in which it's up to you to ensure that you're not breaking invariants.

You'll even get a taste of this with the Observation technique outlined above. You can also write this as:

func withContinousObservation<T>(of value: @escaping @autoclosure () -> T, execute: @escaping (T) -> Void) {
    withObservationTracking {
        execute(value())
    } onChange: {
        Task { @MainActor in
            withContinousObservation(of: value(), execute: execute)
        }
    }
}

In many cases this will be fine, but do remember that the call to execute triggered by the observation will not occur in the current event loop cycle. Maybe this is fine for your use case, but maybe it will introduce a subtle bug where some vital flag isn't set in time to determine whether or not some other action will occur.

Or maybe, you want your change to trigger (or not trigger) an animation in SwiftUI? Well, now you're missing the animation transaction deadline (as explained in this post).

Fingers-crossed we'll get a more filled out Observation offering soon.

6 Likes