SwiftUI @Observable: Stop Rebuilding Views That Read Unrelated Properties
ObservableObject invalidates every observing view on any @Published change. SwiftUI's @Observable macro tracks which properties a body actually reads, cutting redundant updates—if you can live with the trade-offs.
27 Jul 2025, 00:14 UTC

The problem: one notification, many rebuilds
When a view model conforms to ObservableObject, every @Published property mutation emits a single objectWillChange notification. SwiftUI responds by invalidating every view subscribed to that object. A badge view that reads only items.count still re-evaluates its body when an unrelated promoCode string changes. On a screen with many observers, those redundant body evaluations add up.
Per-property tracking with @Observable
The Observation framework's @Observable macro changes the dependency model. Instead of one object-level publisher, it records which stored properties a view's body actually accesses during evaluation. Later mutations invalidate only the views that read the affected property. The result is a fine-grained dependency graph rather than a coarse per-object one.
Ownership also shifts. Hold the model with @State in the view that creates it, pass it down as a plain stored property, inject it with @Environment(Model.self), and use @Bindable when a child needs two-way bindings to its properties.
Worked example and migration
Consider a CartModel that holds items, a computed total, and a promo code:
@Observable
class CartModel {
var items: [Item] = []
var promoCode: String = ""
var total: Double {
items.reduce(0) { $0 + $1.price }
}
}
A BadgeView reading only items.count and a TotalsView reading total register different dependencies. Mutating promoCode invalidates neither, unless another view actually read that property in its body.
Migrating an existing model follows a predictable mapping:
| ObservableObject pattern | @Observable pattern |
|---|---|
@StateObject var model = CartModel() | @State var model = CartModel() |
@ObservedObject var model: CartModel | var model: CartModel (or @Bindable var model: CartModel) |
@EnvironmentObject var model: CartModel | @Environment(CartModel.self) var model |
class CartModel: ObservableObject | @Observable class CartModel |
Trade-offs, limitations, and verification
@Observable classes do not publish Combine events automatically. Existing objectWillChange subscribers, Combine pipelines, and code depending on Published wrappers need explicit rework or a bridging publisher.
Tracking also depends on property reads happening inside the view body. Values copied into local non-observed state, read outside the body, or hidden behind a non-observable wrapper can silently stop triggering updates.
Because @Observable is a macro, the compiler rewrites your type. When debugging, inspect the expanded source rather than assuming the original declaration is what executes.
Observation is version-gated. Confirm your minimum deployment target supports the Observation framework and the @Observable macro before adopting it in a shipping app, and check Apple's current documentation for exact availability. To verify the behavior in your own project, add a signpost or simple log to each view body, mutate one property, and confirm which bodies re-run. Measure with Instruments rather than relying on assumptions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.