The Observation framework provides a macro based implementation of the observer design pattern. It’s designed to be used with SwiftUI and available in the Swift standard library since iOS 17.
Usage
To use the Observation framework in SwiftUI annotate your data with the macro @Observable. Note that @Observable can only be used with classes, not structs or other types.
Property changes automatically refresh the layout, even if the property
is used through a computed property
is nested through several @Observable objects
is outside the view hierarchy (e.g. a global variable or singleton)
Changes are only tracked for properties involved in the layout. For instance, referencing the property in a button action doesn’t track the property.
Property Wrappers
Here is the new (iOS 17+) way to build your views:
Use @State for local state.
Use @Bindable if you need two-way binding ($ operator) to an Observable object.
Use @ObservationIgnored in the properties of an @Observable object to opt-out tracking.
Don’t use any property wrapper if you are just displaying properties from an Observable object.
Example.
Property wrapper use
Migration
Replace ObservableObject
Use @Observable macro instead
Remove @Published on properties
If class has incompatible annotation (like @ModelActor), conform to Observable protocol instead
Example
Replace @StateObject
Use @State instead
@State now handles both value and reference types
Example
Replace @ObservedObject
Use @Bindable for @Observable classes when you need binding syntax (the $ operator)
withObservationTracking is the only programmatic use allowed in this framework. It fires just once, so if you want continuous observation you’ll need a recursive call to re-establishes observation on every change (see below). Example.
A view renders data from the Person Observable object.
SwiftUI runtime reads the code, notices it has an observable object, and creates a tracking wrapper similar to this:
During view rendering, the getter for name triggers an access call:
This call records in thread-local storage that this property was accessed during the view's render. Internally, the ObservationRegistrar maintains a mapping linking this specific property (object + keyPath) to the view update function provided in the onChange callback.
When name is modified:
willSet and didSet are called
The registrar looks up all observers (view update functions) registered for this property
Each registered observer is notified, which in this case triggers SwiftUI to schedule the view for refresh using the onChange callback registered during the tracking (step 2).
Thread-safety
The framework is thread-safe to prevent data races, which supports the use of withObservationTracking outside SwiftUI. The ObservationRegistrar uses an internal _ManagedCriticalState which wraps a mutex (using the new Swift 6 Mutex). All state mutations in ObservationRegistrar happen within this critical region:
willSet
didSet
access tracking
registration of observers
A takeaway point is that while mutations are thread-safe, we still need to mutate only on the main thread for objects used in SwiftUI. While SwiftUI might have internal mechanisms to handle updates from a background thread, it’s not documented or guaranteed. The safer approach is to mutate Observable properties on the main thread, especially when they drive UI updates.
The beginning of a general framework
…the current implementation of Observable was pretty much tailor made for SwiftUI (so much so that I wonder if even withObservationTracking(_:onChange:) should have been under SPI...) Anyway, the plan is to enhance it for more general use-cases – but that hasn't happened yet. -source
Looking at the private enum ObservationKind we see several events:
willSet, didSet could be useful for
Cascading updates where order matters
Debugging and logging
State validation before changes
Transaction management
Undo/redo systems
ValuesObserver seems designed to implement specialized patterns. Example.
Streaming changes
The following example doesn’t compile because certain Observation APIs aren’t public. Nonetheless, it illustrates how we could listen for changes on arbitrary objects.
Third party implementations
If we look at the swift-perception from Pointfree the mechanism is very similar to Apple’s. However, not having access to the SwiftUI internal magic, clients need to do the wrapping themselves calling WithPerceptionTracking.
ObservationBP is another implementation which replicates Apple’s very closely. It uses this ObservationView.swift wrapper. The refresh is forced through a change in local @State, same trick used by WithPerceptionTracking.
Troubleshooting
Value must conform to Observable
Two common reasons for this:
Primitive values don’t conform to Observable.
Optionals are not not Observable either, even when the wrapped type is.
If you need your data to be optional, consider creating an enum with two states, one of them being the @Observable class that you need to track.
Invalid redeclaration of synthesized property
Problem: @Observable and @Bindable they both try to generate a backing storage property. This happens when we want to track and modify properties on a view model.
Solution: If you need @Bindable also add @ObservationIgnored.
Observation will work as demonstrated in this example.
Example of @ObservationIgnored @Bindable
Updates to the parent and child view happen with each press of the counter, even when the viewmodel uses @ObservationIgnored @Bindable.
Note that in this example let viewModel didn’t need @Bindable. If Counter had been added directly to the view, it would need @Bindable to track the changes, like in the code below. However, it is not the case if the view owns an Observable reference object that owns another reference object where the state is.
The key distinction is:
When a view directly owns an @Observable object it needs @Bindable
When a view owns an @Observable object that contains other @Observable objects it doesn't need @Bindable