Thanks for putting this together. I think it makes sense to push the async parts to a future direction – and has the benefit of simplifying the review.
One question that spills over from the last review: how does the withTracking
mechanism handle chained observations?
Take the example of an external module that exposes a public @Observable
type (ExternalObservable
) . Perhaps it is observed by both a) an internal Observable (InternalObservable
) that, on observing a change, updates some internal state, and b) a SwiftUI View that observes the same property.
Now, if that View is also observing the state on InternalObservable
, can we be guaranteed that the updates will occur in the same run-loop cycle/animation transaction?
Some code to illustrate the question:
// View
struct SomeView: View {
@State private var model = InternalObservable()
var body: some View {
// Here, We're observing one property on the external
// Observable directly, and one property indirectly via
// the Internal Observable as an intermediary.
//
// If we cause the properties to be updated at exactly the
// same time (via calling `updateBothProps() for instance)
// we want the view update for both properties to occur at
// exactly the same time, too (the same animation
// transaction/view update/run loop cycle.)
VStack {
Text("\(model.external.directlyObservedProperty)")
Text("\(model.dependentProperty)")
}
}
}
// Internal Observable
@Observable
final class InternalObservable {
var dependentProperty = "Some Value"
let external = ExternalObservable()
init() {
resetObservationOnExternalObservable()
}
// We're observing the external Observable, and updating
// some local state which in-turn is observed by the view.
// Yes, it would be simpler as a computed property, but
// perhaps we want to limit view updates, or alter the
// observation, or some other view invariant we wish to
// maintain.
func resetObservationOnExternalObservable() {
ObservationTracking.withTracking {
self.dependentProperty = "\(external.indirectlyObservedProperty). Woohoo!"
} onChange: { [weak self] in
self?.resetObservationOnExternalObservable()
}
}
}
// Some other module
@Observable
public final class ExternalObservable {
public var directlyObservedProperty = "Some Value"
public var indirectlyObservedProperty = "Some Value"
// Calling this method affects both text fields in the View.
// Hopefully the changes are guaranteed to be coalesced into
// a single view update!
public func updateBothProps() {
self.directlyObservedProperty = "I'm changing"
self.indirectlyObservedProperty = "And so am I!"
}
}
Obviously this is a contrived example and can be simplified using a simple computed property, but hopefully works for illustration purposes.
The feeling I got from the last thread was that this wasn't quite as simple as one might hope:
I'd be interested to know if the revised implementation resolves this.