Explain @State, @Binding, @Bindable and @Environment. When do you use each?
TL;DR
Every piece of data needs one source of truth. Ask: "who owns it?"
- The view owns it:
@State. - A parent owns it and the child must change it:
@Binding. - It is an
@Observableobject and you need bindings to its properties:@Bindable. - It is shared by many views down the tree:
@Environment.
@State: the view owns it
struct Counter: View {
@State private var count = 0
var body: some View {
Button("Count: \(count)") { count += 1 }
}
}SwiftUI stores the value outside the struct and keeps it while the view has the same identity.
Make @State private. Set its first value in the declaration, not from the parent's init.
@State also owns @Observable objects (iOS 17+):
struct ProfileScreen: View {
@State private var model = ProfileModel()
var body: some View { ProfileForm(model: model) }
}@Binding: read and write someone else's data
struct ToggleRow: View {
let title: String
@Binding var isOn: Bool
var body: some View {
Toggle(title, isOn: $isOn)
}
}
struct Settings: View {
@State private var notifications = true
var body: some View {
ToggleRow(title: "Notifications", isOn: $notifications)
}
}The $ prefix gives you the binding (the "projected value").
A binding does not store data. It is a getter and a setter pointing to the real owner.
@Bindable: bindings to an @Observable object
@Observable final class ProfileModel {
var name = ""
var bio = ""
}
struct ProfileForm: View {
@Bindable var model: ProfileModel
var body: some View {
Form {
TextField("Name", text: $model.name)
TextField("Bio", text: $model.bio)
}
}
}If the view only reads the model, a plain let model: ProfileModel is enough. Use @Bindable only when you need $.
@Environment: shared values from above
@main
struct MyApp: App {
@State private var session = Session()
var body: some Scene {
WindowGroup {
RootView().environment(session)
}
}
}
struct AccountBadge: View {
@Environment(Session.self) private var session
@Environment(\.dismiss) private var dismiss
var body: some View {
Text(session.userName)
}
}Use it for app-wide things: the session, theme, dismiss, colorScheme, locale.
Custom keys are easy with the @Entry macro:
extension EnvironmentValues {
@Entry var accentStyle: Color = .blue
}The old way (still asked)
Before iOS 17, reference types used ObservableObject and @Published:
| iOS 17+ (Observation) | Before iOS 17 (Combine based) |
|---|---|
@State var model = Model() | @StateObject var model = Model() |
let model: Model / @Bindable | @ObservedObject var model: Model |
@Environment(Model.self) | @EnvironmentObject var model: Model |
The big bug in the old world: using @ObservedObject to create an object. The view recreates it on every parent update and state is lost. @StateObject fixes that.
Common mistakes
- Two sources of truth: copying a parent's value into child
@State. The child stops seeing updates. - Passing
@Statevalues intoinitand expecting them to reset.@Statekeeps its first value. - Putting everything in the environment. It hides dependencies.
Common follow-up questions
- Why must
@Statebe private? It is owned by this view. Outside code should use a binding. - Can
@Statehold a class? Yes, with@Observableclasses. SwiftUI keeps one instance for the view's lifetime. - What is
Binding.constant(_:)? A fake binding for previews. - What is
@AppStorage? A property wrapper that reads and writesUserDefaultsand updates the view. - What happens when
@Statechanges? SwiftUI callsbodyagain for views that read it and updates only what changed.