Question 6
SwiftUI state — @State, @Binding, @ObservedObject, @EnvironmentObject
When would you use @State vs @ObservedObject vs @StateObject?
Follow-ups
- What about @Observable in iOS 17+?
Answer outline
Each wrapper answers: where does this value live, and who creates it?
Principles
- Who creates the
ObservableObject? That view uses@StateObject. Everyone else@ObservedObject. - One source of truth: either
@State/@StateObjectat the owner, or@EnvironmentObjectat the root. Don’t duplicate with a second@Statecopy of the same model.
@State holds value-type state that this view owns. SwiftUI stores it outside the struct so it survives body recomputation.
struct Counter: View {
@State private var count = 0
var body: some View {
Button("Count: \(count)") { count += 1 }
}
}
Binding<T> is a read/write handle to a value owned elsewhere (usually the parent’s @State). The $ prefix turns a property into a binding when you pass it down.
struct Parent: View {
@State private var isOn = false
var body: some View { ToggleRow(isOn: $isOn) }
}
struct ToggleRow: View {
@Binding var isOn: Bool
var body: some View { Toggle("On", isOn: $isOn) }
}
Use @StateObject when this view creates the ObservableObject. SwiftUI constructs it once for that view’s lifetime (stable identity).
struct Root: View {
@StateObject private var model = ScreenModel()
var body: some View { Dashboard(model: model) }
}
Use @ObservedObject when another view already created the object and passed it in. This view only observes it—do not assign a new instance in init or body.
struct Dashboard: View {
@ObservedObject var model: ScreenModel
var body: some View { Text(model.title) }
}
@EnvironmentObject reads a shared object injected above the subtree with environmentObject(_:) (e.g. Root().environmentObject(SessionStore())). If nothing was injected, the app crashes when the view appears.
struct Profile: View {
@EnvironmentObject private var session: SessionStore
var body: some View { Text(session.userName) }
}
@Observable (Swift 5.9+) is the macro alternative to ObservableObject / @Published. @Bindable lets you form bindings to observable properties on that model.
@Observable
final class Cart { var items: [Item] = [] }
struct CartView: View {
@Bindable var cart: Cart
var body: some View { List(cart.items) { ... } }
}
Follow-up angles
- Wrong tool:
@ObservedObject var m = ScreenModel()inbody→ new model every redraw → empty@Publishedfields. - Previews: pass
.environmentObject(MockSession())or your graph is incomplete.