Skip to content

Observation Framework (Swift)

A SwiftUI guide for coding agents. Also covers observation framework, withobservationtracking, observable macro data flow, bindable property wrapper, replace observedobject, environment observable type.

The Observation framework, built on Swift macros, replaces Combine-based data flow. Mark a class @Observable and any property read becomes trackable, in SwiftUI automatically and elsewhere through withObservationTracking.

Observing Outside SwiftUI

withObservationTracking takes two closures: the first reads the properties you care about, the second fires when any of those properties changes.

withObservationTracking {
    render(store.state)
} onChange: {
    print("State changed")
}

Two gotchas live here. The onChange closure fires only once, so re-register recursively for continuous observation. And it runs before the change applies, so defer the re-read into a new task.

func startObservation() {
    withObservationTracking {
        render(store.state)
    } onChange: {
        Task { startObservation() }
    }
}

Observing in SwiftUI

SwiftUI tracks any observable property the body reads. A plain let on the view is enough; no property wrapper.

struct ProductsView: View {
    let store: Store<AppState, AppAction>

    var body: some View {
        List(store.state.products, id: \.self) { Text($0) }
            .onAppear { store.send(.fetch) }
    }
}

The wrapper set collapses: @State replaces @StateObject for owning the object across the view lifecycle, and @Environment(Store.self) with .environment(store) replaces the EnvironmentObject pair.

Bindings with @Bindable

@Bindable derives bindings from an observable type's properties, for text fields and other two-way controls.

struct AuthView: View {
    @Bindable var viewModel: AuthViewModel

    var body: some View {
        TextField("username", text: $viewModel.username)
        SecureField("password", text: $viewModel.password)
    }
}

When the object arrives through the environment, declare @Bindable inline at the top of the body instead.

struct InlineAuthView: View {
    @Environment(AuthViewModel.self) var viewModel

    var body: some View {
        @Bindable var viewModel = viewModel
        TextField("username", text: $viewModel.username)
    }
}

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-mastering-observable-framework-in-swift" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems