Skip to content
SwiftMind

Observation

@Observable, and how SwiftUI learns which views depend on which properties.

AdvancedNot started8 min
On this page

@Observable is a macro that makes a reference type's properties trackable. A SwiftUI view that reads one of those properties in body is automatically re-rendered when *that property* changes — and not when others do.

That per-property precision is the whole improvement over ObservableObject. With @Published, any change fired objectWillChange, so every observing view re-rendered regardless of what it actually read. With Observation, a view reading only name doesn't care that progress changed.

It also removes the property-wrapper bookkeeping. No @Published on each property, and in a view you use plain let, or @State when the view owns the model's lifetime.

@State to own the model, plain let to pass it, @Bindable when you need $.
@Observablefinal class ProfileModel {    var name = ""            // no @Published needed    var progress = 0.0    private(set) var loadCount = 0
    func reload() { loadCount += 1 }}
struct NameView: View {    let model: ProfileModel          // plain let — tracking is automatic
    // reads `name` only, so a change to `progress` does not re-render this    var body: some View { Text(model.name) }}
struct OwnerView: View {    @State private var model = ProfileModel()   // this view owns the lifetime
    var body: some View {        VStack {            NameView(model: model)            // @Bindable projects bindings from an @Observable model            @Bindable var model = model            TextField("Name", text: $model.name)        }    }}
Show all 28 lines
Per-property dependency tracking
ProfileModel@Observable
namechanged
progressunchanged
NameViewre-renders
ProgressViewuntouched

Two views read different properties of one Observable model. Changing one property re-renders only the view that read it.

Each view's body reads specific properties, and the framework records which.

Step 1 of 3
  1. Each view's body reads specific properties, and the framework records which.
  2. Only `name` changes.
  3. Only the view that read `name` is invalidated. The other doesn't re-render.

Under the hood

The macro rewrites each stored property into a computed one that calls access(keyPath:) on read and withMutation(keyPath:) on write, backed by an ObservationRegistrar.

SwiftUI evaluates body inside withObservationTracking, so every property accessed during that evaluation is recorded as a dependency of that view. When one of those properties is mutated, the registrar notifies exactly the views that read it.

Two consequences worth internalising. First, tracking is per *access during body*, so a property read in a method you call from body is still tracked, while one read in a button's action closure is not — that closure isn't part of body evaluation. Second, reading a whole collection tracks the collection, so appending to an array re-renders everything that read it, no matter how granular the model looks.

It's also not SwiftUI-specific: withObservationTracking is in the Observation module and usable from plain Swift.

Quick check

What's the main advantage of @Observable over ObservableObject with @Published?

  • It works with structs as well as classes
  • Dependencies are tracked per property, so a view only re-renders when a property it actually read changes
  • It removes the need for @State anywhere
  • It makes the model thread-safe
Reveal answer

Dependencies are tracked per property, so a view only re-renders when a property it actually read changes

ObservableObject fires one objectWillChange for any mutation, so every observing view re-renders regardless of what it read. Observation records the specific properties accessed during body evaluation and notifies only those views. It's still classes only, it doesn't touch thread safety, and you still use @State to own a model's lifetime.

Interview question

How does SwiftUI know which views to re-render when an @Observable model changes?

Think first, then reveal

It records dependencies by observing what each view actually reads. SwiftUI evaluates body inside withObservationTracking, and the macro has rewritten every stored property into a computed one that reports reads to an ObservationRegistrar. So by the end of evaluating a body, the framework has an exact set of properties that view depends on. Mutating one goes through withMutation, which notifies precisely the views that read it.

The contrast with ObservableObject is the granularity. There, any @Published write fires a single objectWillChange on the whole object, so every view observing it re-renders whether or not the change was relevant. In a screen with one model and twenty views, that's nineteen wasted body evaluations per keystroke. Observation makes that a non-issue without anyone hand-splitting models to work around it.

Two details I'd flag. Tracking is scoped to body evaluation, so a read in a button's action closure isn't a dependency — which is normally what you want. And granularity is per property, not per element: if a body reads an array, appending to it invalidates that view even if it only rendered one element. Fine-grained models don't help there; you need the view structure to match, typically by putting a ForEach row in its own view that reads only its element.

The practical upshot is that most gratuitous re-render problems go away, and the ones left are about view decomposition rather than about observation.

What an interviewer listens for

  • Explains withObservationTracking and the registrar accurately
  • Quantifies the improvement over objectWillChange
  • Knows tracking is scoped to body evaluation, not closures
  • Raises collection granularity as the remaining sharp edge
  • Connects the fix to view decomposition

Key takeaway

@Observable tracks reads at property granularity during body evaluation, so only views that actually read a property re-render when it changes. Own the model where its lifetime belongs, and pass it down plainly.

Read these first

Related

0%
Quiz this concept