Greetings, traveler!
SwiftUI automatically tracks changes to observable models inside body, so most apps never need manual observation. But outside SwiftUI, things are different. Controllers, coordinators, caches, services, and SwiftData infrastructure often need to react to model changes continuously.
iOS 27 introduces withContinuousObservation(options:apply:), a new Observation API that does exactly that. It continuously tracks changes, automatically re-registers dependencies after every update, and keeps running until you cancel the returned token.
Why this API exists
Before iOS 27, withObservationTracking only observed the next change. If you wanted continuous observation, you had to register it again every time:
func observe() {
withObservationTracking {
_ = model.status
} onChange: {
observe()
handleStatusChange()
}
}While this works, it has several drawbacks:
- You must manually re-register observation.
- Managing the observation lifetime is your responsibility.
- The API primarily targets one-shot observation.
withContinuousObservation removes that boilerplate by automatically re-arming observation after every change.
Getting started
The API returns an ObservationTracking.Token. As long as you keep that token alive, observation continues.
@Observable
final class Order {
var status = "Pending"
}
@MainActor
final class OrderWatcher {
private var token: ObservationTracking.Token?
init(order: Order) {
token = withContinuousObservation(options: [.didSet]) { event in
let status = order.status
guard event.matches(\Order.status) else {
return
}
print("Order status:", status)
}
}
}Unlike withObservationTracking, no recursive re-registration is necessary.
Dependencies are discovered automatically
Observation only tracks properties that are actually read inside the closure.
token = withContinuousObservation(options: [.didSet]) { _ in
let status = order.status
updateUI(status)
}Only status becomes a dependency. If progress changes, the closure will not run because it was never accessed.
This also means dependencies can change dynamically.
token = withContinuousObservation(options: [.didSet]) { _ in
if settings.showsDetails {
render(model.details)
}
}When showsDetails is false, details is not observed. Once the condition becomes true, the next execution registers details automatically.
One important rule is that the property must be read before any early return. Otherwise it won’t be registered during that execution.
Observation options
The API lets you choose when notifications should be delivered.
.willSet
.didSet
.deinitThese options can be combined:
[.willSet, .didSet]willSetruns before a property changes.didSetruns after the new value has been stored.deinitnotifies when an observed object is deallocated.
For most application code, .didSet is the natural choice.
Working with ObservationTracking.Event
The closure receives an ObservationTracking.Event describing why it was triggered.
The most useful method is:
event.matches(\Order.status)This lets you determine which tracked property caused the callback.
If multiple properties are observed, you can handle each independently without creating multiple observers.
Managing the observation lifetime
The returned token owns the observation.
private var token: ObservationTracking.Token?When the token is released or cancelled, observation stops.
token?.cancel()
token = nilCreating the observer without storing the token immediately ends its lifetime, making the observation effectively useless.
Actor isolation
The observation closure inherits the actor isolation of the context where it is created.
For example, creating the observer inside a @MainActor type guarantees that every callback executes on the main actor.
This makes the API a good fit for UIKit and AppKit controllers that update UI directly.
Using it with SwiftData
Although withContinuousObservation belongs to the Observation framework, it works particularly well with SwiftData.
Apple demonstrates it together with ResultsObserver, allowing code outside SwiftUI to react whenever query results change.
Typical examples include:
- updating map annotations
- synchronizing caches
- reacting to background model changes
- building custom infrastructure around SwiftData
This fills an important gap for projects that cannot rely on @Query.
How it compares to other Observation APIs
Each Observation API targets a different scenario.
| API | Best used for |
|---|---|
| SwiftUI automatic observation | Updating SwiftUI views |
withObservationTracking | One-shot observation |
Observations | Asynchronous observation via AsyncSequence |
withContinuousObservation | Long-lived synchronous observation outside SwiftUI |
When to use it
withContinuousObservation is a good choice when building:
- UIKit or AppKit bindings
- coordinators and controllers
- caches
- synchronization services
- SwiftData infrastructure outside SwiftUI
You usually do not need it inside SwiftUI views because the framework already performs dependency tracking automatically.
Final thoughts
withContinuousObservation removes one of the biggest limitations of the original Observation API. Instead of manually re-registering observation after every change, you create it once, keep the returned token alive, and let the framework handle the rest.
If you’re building infrastructure around @Observable models or integrating SwiftData outside SwiftUI, this is likely the Observation API you’ll reach for most often.
