Exploring Observation in Swift: What Happens with Private Properties


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 type ObservationRegistrar. This is the runtime piece responsible for managing observers.
  • For observableProperty, the compiler generated getter, setter, and _modify methods that wrap reads and writes with calls to access(keyPath:) and withMutation(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 marked private, the compiler still generated the same access and mutation boilerplate as for public properties. The getter and setter 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.