Greetings, traveler!
Swift’s new Observation
framework simplifies the way we track state changes across models. By marking a class with @Observable
, we automatically get publisher-like behavior for its properties without manually wiring up @Published
. But what happens when the property is private? Does the compiler still generate the boilerplate code for observation? Let’s walk through how this can be verified.
The Example
Here is a minimal example that includes three different kinds of properties:
import Foundation
@Observable
final class ViewModel {
var observableProperty: String = "Hello, World!"
@ObservationIgnored
var observationIgnoredProperty: String = "Hello, World!"
private var privateProperty: String = "Hello, World!"
}
This class contains:
- A regular observable property
- A property explicitly excluded with
@ObservationIgnored
- A private property with no annotation
The question is whether the private property still gets the observation boilerplate.
Introducing SIL
Swift compiles code down through several stages before producing machine code. One of these stages is SIL (Swift Intermediate Language). SIL is a textual, intermediate representation that sits between the high-level AST and LLVM IR. By looking at SIL, we can see what additional code the compiler generates, including the synthesized accessors and hooks used by Observation
.
To get SIL output, you can run the following command in Terminal:
xcrun swiftc -emit-silgen -Onone -parse-as-library \
-sdk $(xcrun --show-sdk-path --sdk macosx) \
ViewModel.swift > ViewModel.sil
The -emit-silgen
flag produces a higher-level SIL representation, showing the boilerplate before optimization removes or simplifies it.
What We Found
Inspecting the generated ViewModel.sil
reveals the following:
- The class contains a hidden field called
_$observationRegistrar
of typeObservationRegistrar
. This is the runtime piece responsible for managing observers. - For
observableProperty
, the compiler generatedgetter
,setter
, and_modify
methods that wrap reads and writes with calls toaccess(keyPath:)
andwithMutation(keyPath:_:)
. This ensures observers are notified on every access or mutation. - For
observationIgnoredProperty
, those calls are absent. Accessors directly read and write the storage without involving the registrar. - For
privateProperty
, despite being markedprivate
, the compiler still generated the same access and mutation boilerplate as for public properties. Thegetter
andsetter
both interact with the registrar, which means the property participates in observation just like any other.
ViewModel.sil -> https://github.com/Livsy90/ObservationExploration/blob/main/ObservationExploration/ViewModel.sil
Conclusion
The experiment shows that private properties in an @Observable
class are still observable unless explicitly marked with @ObservationIgnored
. Visibility modifiers such as private
do not exclude them from observation. If the intent is to keep a property internal to the class without creating any boilerplate code under the hood, @ObservationIgnored
should be applied.
Looking at SIL is a useful way to understand what the compiler generates behind the scenes, but it is not strictly required. If you only need to quickly confirm which properties are tracked, using Xcode’s Expand Macro feature is often enough, since it reveals the synthesized @ObservationTracked
attributes directly.
If you enjoyed this article, please feel free to follow me on my social media: