withContinuousObservation in Swift


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
.deinit

These options can be combined:

[.willSet, .didSet]
  • willSet runs before a property changes.
  • didSet runs after the new value has been stored.
  • deinit notifies 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 = nil

Creating 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.

APIBest used for
SwiftUI automatic observationUpdating SwiftUI views
withObservationTrackingOne-shot observation
ObservationsAsynchronous observation via AsyncSequence
withContinuousObservationLong-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.