Question 6
SwiftUI state: @State, @StateObject, and @ObservedObject
When would you use @State vs @ObservedObject vs @StateObject?
Follow-ups
- What about
@Observableon iOS 17 and later?
Answer outline
Each wrapper answers two questions: where the value lives, and who creates it. The three in the question differ in who owns the state:
- 1.Local state:
@Stateholds state this view owns, usually a value type. SwiftUI stores it outside the struct, so it survivesbodyrecomputation. - 2.Owned object:
@StateObjectis for the view that creates anObservableObject. SwiftUI constructs it once for that view's lifetime and keeps it alive across redraws. - 3.Borrowed object:
@ObservedObjectis for a view that receives an object someone else created. It only observes, so never assign a new instance ininitorbody.
@Binding gives a child read and write access to a value the parent owns, and @EnvironmentObject reads a shared object injected higher up the tree. On iOS 17 and later, @Observable replaces most ObservableObject wiring, with @State owning the model and @Bindable producing bindings.
Principles
- Whoever creates the
ObservableObjectuses@StateObject, and everyone else uses@ObservedObject. @Stateowns this view's state, and@Bindingis the child's handle to a value the parent owns.- Keep a single source of truth: the owner holds
@Stateor@StateObject, and nothing else keeps a copy. @ObservedObjectnever creates the object it observes, because a fresh one would be made each time the parent rebuilds this view.
A counter whose value lives in @State and survives every body recomputation:
struct Counter: View {
@State private var count = 0
var body: some View {
Button("Count: \(count)") { count += 1 }
}
}
The parent owns isOn and passes $isOn down, which produces a Binding the child can read and write:
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, because SwiftUI constructs it once for the view's lifetime:
struct Root: View {
@StateObject private var model = ScreenModel()
var body: some View { Dashboard(model: model) }
}
Use @ObservedObject when another view created the object and passed it in, and never 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(_:), and the app crashes on appear if nothing was injected:
struct Profile: View {
@EnvironmentObject private var session: SessionStore
var body: some View { Text(session.userName) }
}
@Observable (iOS 17 and later) is the macro alternative to ObservableObject and @Published, and @Bindable forms bindings to its properties:
@Observable
final class Cart { var items: [Item] = [] }
struct CartView: View {
@Bindable var cart: Cart
var body: some View { List(cart.items) { item in Text(item.name) } }
}
Follow-up angles
@ObservedObject var model = ScreenModel()builds a new model every time the parent re-creates this view, so@Publishedfields keep resetting to their initial values.- Previews need the same injection as the app, so pass
.environmentObject(MockSession())or the preview crashes on appear.



