[Pitch] Advanced Observation Tracking

Thanks Philippe!

Is the expected usage pattern for receiving any deinit event “go reevaluate all state being tracked” (in the SwiftUI example this is presumably View/body)? Some elaboration on why the deinit event is useful would be a useful addition to the pitch text, as I’m not familiar with another observation system that exposes this hook.

Is there no place to break that cycle manually after the event has been emitted to clients? I’d expect the observation internals to own the lifetime of that event instance, modulo a client who escapes the event from their tracking closure (which would surprise me, or is that a valid usage pattern?).

Sorry, let me clarify the scenario. It’s fine for Parent to have its own observable state and I’m not worried about replacement of the value of child, only the propagation of an underlying change in the Child instance:

// Intentionally not exposed to clients
@Observable private class Child {
    var childValue = 0
}

@Observable public class Parent {
    private var parentValue = 0 // Independent Observable state at the parent level, not that a client could know!

    // Child is strictly an implementation detail
    private let child = Child()

    public init() {}

    // Computed property of interest to a client
    public var totalValue: Int { child.childValue + parentValue } 

    public func incrementChild() { 
        // Produces an observable change in `totalValue`
        child.childValue += 1 
    } 
}

Then in the client:

let parent = Parent()
let token = withObservationTracking(options: [.willSet, .didSet]) {
    parent.totalValue
} onChange: { event in 
    // `false`, but this is the only property whose existence I know about as a client!
    // `true` if `totalValue` were a stored property (e.g. a cache of its computed value) instead
    event.matches(\Parent.totalValue)
}

// Some mutation to parent happens elsewhere, intending to be observed by the above:
parent.incrementChild()

My point is that the ability to depend on matches(_:) to disambiguate between changes to the observed object (the reason for matches(_:) to exist, to my understanding) depends on observed properties being stored, not computed. (My assumption here is that the @Observable macro only applies automatic tracking to declared stored properties, but please correct me if that has changed.)

Swift tries hard to mask over whether a property is stored or computed across a module boundary, so an otherwise API- and ABI-compatible change to turn a stored property into a computed one (or vice versa) would change the behavior of matches(_:), which is subtle.

This explanation helps, thanks! I’d find a note like this valuable in the pitch text, perhaps alongside a discussion (or rejection) of whether the type representing options for continuous observation ought to be different from the type representing options for non-continuous observation.

1 Like