@Observable replaces ObservableObject: what actually changes
ObservableObject aged well for its era, but it has a structural flaw: a view
observing an object redraws as soon as any of its @Published properties
changes - including the ones it never displays.
@Observable fixes that at the compiler level. Here is what changes in practice.
The global re-render problem
With ObservableObject, the subscription happens at the object level, not the
property level. A screen that only displays user.name redraws when
user.lastSyncDate changes in the background.
On a list of 200 cells sharing the same store, you can see it.
What @Observable does
The macro instruments every property. SwiftUI then records exactly which
properties were read during body, and invalidates the view only when one of
those changes.
@Observable
@MainActor
final class LibraryStore {
var books: [Book] = []
var lastSyncDate: Date?
}
No @Published, no objectWillChange. Tracking is automatic.
How to migrate a screen
- Replace
ObservableObjectwith@Observable, delete every@Published. @StateObjectbecomes@State,@ObservedObjectgoes away (a plain property is enough),@EnvironmentObjectbecomes@Environment.- For a binding to the object, use
@Bindable.
struct LibraryView: View {
@State private var store = LibraryStore()
var body: some View {
List(store.books) { book in
Text(book.title)
}
}
}
The traps
- Properties read outside
bodyare not tracked. If the value is read in a closure that runs later, observation never fires. @Observableis not thread-safe. Mark the class@MainActorunless the project already enables global main-actor isolation.- No retroactive conformance. A type cannot be both
@ObservableandObservableObject, so migration happens one full screen at a time.
Verdict
On a simple screen the gain is invisible. On a list fed by a shared store the difference in smoothness is immediate - and the code sheds a third of its annotations along the way.