Observation Macros (@Observable / @Bindable) (iOS 17+)

Lightweight SwiftUI observation introduced in iOS 17 / macOS 14. Replaces many ObservableObject boilerplates; still opt-in depending on deployment target.

Version Quick Reference

  • iOS 17, macOS 14, watchOS 10, tvOS 17: @Observable and @Bindable.
  • Older OS targets: stick with ObservableObject, @Published, @StateObject/@ObservedObject.

Basic Usage

  • Mark a reference type (or actor) with @Observable to synthesize change notifications for stored properties.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Observable
final class CounterModel {
    var count = 0
    func increment() { count += 1 }
}

struct CounterView: View {
    @State private var model = CounterModel()
    var body: some View {
        VStack {
            Text("\(model.count)")
            Button("Plus") { model.increment() }
        }
    }
}
  • No need for ObservableObject or @Published; mutation on observed properties triggers view updates.

@Bindable

  • Use @Bindable in child views to get bindings into an @Observable model without manually creating Binding values.
1
2
3
4
5
6
struct DetailView: View {
    @Bindable var model: CounterModel
    var body: some View {
        Stepper("Count", value: $model.count)
    }
}

Interop with Existing Patterns

  • You can mix @Observable models with @StateObject when you own the lifetime, or pass them down with @ObservedObject-like semantics using plain references (the model itself performs observation).
  • For legacy targets, keep an ObservableObject wrapper or use conditional compilation:
1
2
3
4
5
#if canImport(Observation)
@Observable final class ProfileModel { var name = "" }
#else
final class ProfileModel: ObservableObject { @Published var name = "" }
#endif

Concurrency Notes

  • Observation macros work with actors too; the generated conformance handles main-actor isolation if your type is @MainActor.
  • Avoid heavy work in property observers; keep state updates lightweight and move side effects to methods.

Testing

  • In previews/tests, mutate properties and assert UI/output changes. For legacy fallback, ensure both code paths stay in sync using small compile-time helpers or conditional tests.