Unable to manually attach ObservationTracked. Is this intended behavior?

Hi! I'm currently (5.10 and 5.11) unable to manually attach an ObservationTracked macro to an Observable class:

import Observation

@available(macOS 14.0, *)
@available(iOS 17.0, *)
@Observable class Person {
  @ObservationTracked var name: String?
}

This expands:

@available(macOS 14.0, *)
class Person {
  var name: String?{
    @storageRestrictions(initializes: _name)  //  Cannot find type '_name' in scope
    init(initialValue) {
      _name = initialValue                    //  Cannot find type '_name' in scope
    }
    get {
      access(keyPath: \.name)
      return _name                            //  Cannot find type '_name' in scope
    }
    set {
      withMutation(keyPath: \.name) {         //  Cannot find type '_name' in scope
        _name = newValue
      }
    }
  }
  
  
  @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
  
  internal nonisolated func access<Member>(
    keyPath: KeyPath<Person , Member>
  ) {
    _$observationRegistrar.access(self, keyPath: keyPath)
  }
  
  internal nonisolated func withMutation<Member, MutationResult>(
    keyPath: KeyPath<Person , Member>,
    _ mutation: () throws -> MutationResult
  ) rethrows -> MutationResult {
    try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
  }
}

@available(macOS 14.0, *) extension Person: Observation.Observable {
}

Attaching the ObservationTracked macro manually seems to be blocking the Observable macro on synthesizing the _name stored variable.[1]

Removing the ObservationTracked macro gets things working again… so I'm not blocked on anything… but is this the intended behavior? The ObservationTracked docs tell us[2]:

The [Observable macro] uses this macro. Its use outside of the framework isn’t necessary.

It's ambiguous to me whether or not that means (to an engineer) "you can use this but you shouldn't have to" or "we don't expect you to use this". Is this a "bug" worth fixing or is this just something that works as intended for now?


  1. swift/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift at swift-5.10-RELEASE · apple/swift · GitHub ↩︎

  2. ObservationTracked() | Apple Developer Documentation ↩︎

I haven't used @Observable in my project, but I think this is the intended behavior. @Observable macro adds ObservationTracked macro to all stored properties unless they have ObservationNotTracked macro (I forgot the exact name) applied. So you are not supposed to add ObservationTracked macro manually.

How about think like this: it's an internal macro and should only be used by @Observable macro.

1 Like

Yeah… that seems to be what I am seeing so far. Oh well… it's not blocking me. Thanks!

If you use it manually you must preform ALL of the tasks the macro does - e.g. creating a storage ivar. It is there to be a marker such that other macros can utilize it. In common usage I would not expect folks to ever type @ObservationTracked in their source code, but I would expect that other macros that utilize observation to emit @ObservationTracked to prevent recursion and identify the tracked variables such that it can synthesize parameters later in the application of the macro.

2 Likes

Hmm… I'm actually not completely sure I follow exactly. I can see the ObservationTrackedMacro macro following two roles: PeerMacro and AccessorMacro. For the PeerMacro role, ObservationTrackedMacro does skip out early when it detects ObservationTracked (or ObservationIgnored) was added.[1] For the AccessorMacro role, ObservationTrackedMacro does not skip out early when it detects ObservationTracked.[2] This matches up with what I saw originally… adding the macro by hand in code led to synthesized getter and setter but no synthesized ivar.

ObservableMacro follows the MemberAttributeMacro role and skips out on attaching ObservationTracked when it detects the property is already ObservationTracked.[3]

Given that ObservableMacro is not attaching ObservationTracked when it detects ObservationTracked has already been added… in what way would it be undesirable for the check inside the ObservationTrackedMacro implementation of PeerMacro to not return early if ObservationTracked was detected? And why would it not be undesirable for the check inside the ObservationTrackedMacro implementation of AccessorMacro to not return early if ObservationTracked was detected?

There's a meta-question brewing here that is agnostic of Observable… which is what patterns should engineers practice when writing their own macros that attach (publicly available) property macros directly through MemberAttributeMacro. What (if any) edge-casey or race-conditioney behavior from the macro system should we need to defend against (where stateless codegen functions don't have a simple way to save state and know whether an arbitrary property macro came "from inside" or "from outside").


  1. swift/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift at swift-5.10-RELEASE · apple/swift · GitHub ↩︎

  2. swift/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift at swift-5.10-RELEASE · apple/swift · GitHub ↩︎

  3. swift/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift at swift-5.10-RELEASE · apple/swift · GitHub ↩︎