Gotcha when using ObservationTracking.Event.matches

Looking through some WWDC26 sample code I came across something that was difficult to understand:

activity.token = withContinuousObservation(options: .didSet) { event in
  _ = activity.name
  _ = activity.isComplete
  
  if event.matches(\Activity.name) {
    activity.dateEdited = .now
  }
  
  if event.matches(\Activity.isComplete) {
    activity.dateEdited = .now
    activity.trip?.isComplete = activity.trip?.activities.isEmpty == false
      && activity.trip?.activities.allSatisfy { $0.isComplete } == true
  }
}

It took me a moment to understand that the _ = activity.name and _ = activity.isComplete are necessary to start observing those properties, and then later the observation is concerned about which property was changed via the Event.matches method. That means if you forget to do those _ = lines of code you will break this feature.

This is a little surprising because we are used to having the "magic" of observable just work. You access something inside the trailing closure, and it's observed. But here, matches(KeyPath) does not start observation for that property, nor can it. And I think it's pretty reasonable that 6 months from now you would forget what those lines meant, and then figure it's old debug code left over that should be deleted.

Is there room for an API that can check the event for the mutation of a particular property, and register an access with the registrar? Something like this:

activity.token = withContinuousObservation(options: .didSet) { event in
  if event.matches(activity, \.name) {
    activity.dateEdited = .now
  }
  
  if event.matches(activity, \.isComplete) {
    activity.dateEdited = .now
    activity.trip?.isComplete = activity.trip?.activities.isEmpty == false
      && activity.trip?.activities.allSatisfy { $0.isComplete } == true
  }
}

The act of checking matches(activity, \.name) will also register an access in the registrar so that you no longer need to know to add those _ = lines. And I could still see a use case for matches(_:) for when you don't care about which object had the mutation, just that the mutation occurred.

6 Likes

Hi @Philippe_Hausler, in case you missed this, curious if you think there is room for improvement here?

I saw your post; I still haven't decided that is actually something that we should go after or would it end up having sneaky ramifications; the major problem is that the matches can't access that with the access call; it would have to read it via subscript (which might be cagey).

Besides the implementation or API, what sneaky ramifications do you foresee? And are they any sneakier than what I outlined above? The fact that this is the expected way to check if an event matches a key path and observe the key path seems pretty sneaky:

activity.token = withContinuousObservation(options: .didSet) { event in
  _ = activity.name
  _ = activity.isComplete
  if event.matches(\Activity.name) {
    …
  }
  if event.matches(\Activity.isComplete) {
    …
  }
}

The issues I was worrying about is how the folded optimizations of key path access could work is all - it is something that would need to be verified that it hits the right things. Having it be an overload that takes the object and a key path would help with the fact it would allow developers to choose for the side effect of observation or not (because I could envision folks might want it to be the other way around). But the trickier part is checking that the object in question is the object that is matching or not. Because that would be my expectation.