100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Swift

The @Observable Macro

See how the iOS 17 @Observable macro replaces ObservableObject with automatic, fine-grained property tracking that simplifies SwiftUI model code.

State & Data FlowIntermediate9 min readJul 8, 2026
Analogies

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.

swift
@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.

swift
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

Was this page helpful?

Topics covered

#Swift#IOSWithSwiftUIStudyNotes#MobileDevelopment#TheObservableMacro#Observable#Macro#ObservableObject#State#StudyNotes#SkillVeris