[Second review] SE-0395: Observability

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.