The @Observable Macro
Introduced with iOS 17 and Swift 5.9's macro system, @Observable is Apple's modern replacement for the ObservableObject/@Published/Combine trio. Applying @Observable to a class rewrites it at compile time so every stored property becomes independently trackable, without needing to annotate each one with a wrapper. SwiftUI observes property access directly during body evaluation and only re-renders a view when a property it actually read changes—this fine-grained tracking is more precise than ObservableObject's all-or-nothing objectWillChange signal, and it results in noticeably less boilerplate and fewer unnecessary re-renders.
Cricket analogy: Just as a modern ball-tracking system flags only the specific fielder whose zone the ball entered instead of alerting the whole team on every delivery, @Observable's fine-grained tracking re-renders a view only when a property it actually read changes, unlike ObservableObject's all-or-nothing objectWillChange signal.
From ObservableObject to @Observable
Converting is mostly a matter of deletion: drop the ObservableObject conformance and every @Published wrapper, and add @Observable above the class declaration. Every stored property is automatically observable; you no longer opt properties in individually, and there's no import Combine requirement for the class itself. Reference semantics are unchanged—@Observable still requires a class (or actor, with restrictions), not a struct, because observation depends on identity and mutation of shared storage.
Cricket analogy: Just as retiring an old scoring method doesn't change the fact that a team still needs a captain, one shared entity, converting to @Observable means dropping ObservableObject and @Published but the type still must be a class, since observation depends on shared identity.
@Observable
final class CartViewModel {
var items: [String] = []
var isCheckingOut = false
var itemCount: Int { items.count }
func add(_ item: String) {
items.append(item)
}
}
struct CartScreen: View {
@State private var viewModel = CartViewModel() // note: @State, not @StateObject
var body: some View {
Text("Items: \(viewModel.itemCount)")
Button("Add") { viewModel.add("Widget") }
}
}@State Instead of @StateObject
One of the most notable API changes is that ownership of an @Observable model at its creation site is expressed with plain @State, not @StateObject—@State was generalized in iOS 17 to also persist reference-type @Observable instances across view re-renders, unifying the property wrapper story for both value and reference types. When passing the model to a child view, you simply declare a normal (non-wrapped) property of that type; there is no @ObservedObject equivalent needed, because @Observable's tracking works automatically as long as the child view's body reads the property during rendering.
Cricket analogy: Just as a single official scorebook is kept at the scorer's table and simply shown to any player who wants to check it without a separate duplicate copy, an @Observable model's owning view uses plain @State, and child views just declare a normal property with no @ObservedObject equivalent needed.
struct CartSummary: View {
var viewModel: CartViewModel // plain property—no wrapper needed
var body: some View {
Text("Items: \(viewModel.itemCount)")
}
}The fine-grained tracking means a view reading only viewModel.isCheckingOut will NOT re-render when viewModel.items changes, and vice versa—SwiftUI records exactly which properties body accessed each render and subscribes only to those, unlike ObservableObject where any @Published change re-renders every observer.
@Observable requires a genuinely fresh render read to track a property. If a property is only read inside a closure that doesn't execute during the current body evaluation (e.g. an unused branch, or inside .onAppear rather than the view body itself), changes to it won't necessarily trigger a re-render the way you'd expect from ObservableObject's broader objectWillChange signal.
For Environment-Injected Models
@Observable also works with .environment(_:) (the modern counterpart to .environmentObject(_:)) and @Environment(MyModel.self) for retrieval, giving the same implicit-injection convenience as @EnvironmentObject but with @Observable's fine-grained tracking and, notably, support for optional environment values and multiple distinct types resolved by type key.
Cricket analogy: Just as a stadium can broadcast separate feeds for the home team's stats and the away team's stats, each retrievable by its own channel, @Observable works with .environment(_:) and @Environment(MyModel.self) retrieval, supporting multiple distinct types resolved by type key, including optional values.
- @Observable (iOS 17+) replaces ObservableObject/@Published with compiler-synthesized, per-property observation.
- It must be applied to a class; no Combine import or protocol conformance is required.
- Ownership at the creation site uses plain @State, not @StateObject, unifying value and reference type handling.
- Views receiving the model elsewhere use a plain (unwrapped) property, replacing @ObservedObject.
- Tracking is fine-grained: a view only re-renders when a property it actually read during body changes.
- Environment injection uses .environment(_:) and @Environment(Type.self), the @Observable-era equivalents of .environmentObject.
Practice what you learned
1. What must an @Observable type be?
2. Which property wrapper should own an @Observable model at its point of creation in a SwiftUI view?
3. How does @Observable's re-render tracking differ from ObservableObject's?
4. How does a child view typically receive an @Observable model from a parent?
5. What is the @Observable-era equivalent of @EnvironmentObject and .environmentObject(_:)?
Was this page helpful?
You May Also Like
ObservableObject and @ObservedObject
Learn how the pre-iOS 17 ObservableObject protocol and @Published/@ObservedObject let reference-type models publish changes that drive SwiftUI view updates.
@State and @Binding
Understand how @State owns local view state and how @Binding lets child views read and write that state without owning it themselves.
@EnvironmentObject Basics
Learn how @EnvironmentObject injects a shared model into the SwiftUI view hierarchy so deeply nested views can access it without manual parameter passing.
ViewModel Basics in SwiftUI
A hands-on look at how to actually write, own, and wire up a ViewModel class in a SwiftUI app, using @Observable and @MainActor correctly.