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)
}
}