SwiftUI state & ownership — quick reference
A practical comparison of @State, @Binding, classic ObservableObject wrappers and the Observation model.
Who owns the state?
Start with ownership, not the wrapper name. The view that creates and preserves a value generally owns its lifecycle; dependent child views receive bindings or observable dependencies.
| Wrapper / macro | Ownership | When to use |
|---|---|---|
| @State | Local state stored by the view. | Local values or an @Observable model owned by the view. |
| @Binding | Read/write connection to state owned elsewhere. | Child view edits parent-owned state. |
| @StateObject | View creates and retains a classic ObservableObject. | Long-lived ObservableObject using the older observation model. |
| @ObservedObject | View observes an ObservableObject created elsewhere. | Model injected from parent. |
| @EnvironmentObject | ObservableObject injected into view hierarchy. | Shared observable dependency; use carefully. |
| @Observable | Macro enabling modern property tracking. | Observable reference models with less boilerplate. |
| @Bindable | Provides bindings to properties of an @Observable object. | Inputs and controls editing observable properties. |
Interview reminder
With modern Observation, @State often owns a newly created @Observable model, and @Bindable exposes bindings when a child needs editable properties. With classic ObservableObject, use @StateObject for a view-owned instance and @ObservedObject for externally owned instances.
import SwiftUI
import Observation
@Observable
final class LoginForm {
var email = ""
var password = ""
}
struct LoginView: View {
@State private var form = LoginForm()
var body: some View {
LoginFields(form: form)
}
}
struct LoginFields: View {
@Bindable var form: LoginForm
var body: some View {
TextField("Email", text: $form.email)
}
}Environment, lifetime and two-way data flow
Use @Environment for dependencies supplied by the environment (for example a dismiss action or an injected observable model). @EnvironmentObject belongs to the older ObservableObject model; with Observation, you can inject an @Observable instance through the environment using its type. View-owned @State is transient UI state, not persistent database storage.
import SwiftUI
import Observation
@Observable
final class Session {
var username = "Guest"
}
struct RootView: View {
@State private var session = Session()
var body: some View {
ProfileView().environment(session)
}
}
struct ProfileView: View {
@Environment(Session.self) private var session
var body: some View {
Text(session.username)
}
}Decision rule: a child only reading an observable object's properties normally just needs the model reference. Use @Bindable when a control needs a projected binding. @StateObject and @ObservedObject are still appropriate for older ObservableObject/@Published APIs; mixing them blindly with @Observable is a common mistake.
Pitfalls and trade-offs
Avoid recreating expensive models during View initialization. Place state at the least common owner of consumers, preserve identity through stable view structure, and don't put persistent data directly in @State. Observation is available from iOS 17; lower deployment targets may need compatibility approaches.
Personal study notes. Check details against your target Swift/iOS version and official documentation.