@StateObject and @ObservedObject both connect an ObservableObject to a SwiftUI view. The key difference is ownership: use @StateObject when the view creates the object and @ObservedObject when the view receives it from elsewhere.
Choosing the wrong wrapper can cause your model to be recreated whenever SwiftUI rebuilds a view. I made this mistake myself because both wrappers initially appeared to produce the same result. The difference only becomes visible once your view starts owning mutable state.
@StateObject vs. @ObservedObject
You can make the correct choice by answering one question: does this view create the object?
- Use
@StateObjectwhen the view creates and owns anObservableObject. - Use
@ObservedObjectwhen the view receives anObservableObjectfrom a parent. - Never create an object inline using
@ObservedObject. - Don’t use
@StateObjectin multiple views for the same instance.
Both wrappers trigger view updates when an @Published property changes. However, only @StateObject asks SwiftUI to preserve the object’s lifetime for the view.
What is an @ObservedObject?
The @ObservedObject property wrapper observes an ObservableObject that another view owns. Changes published by the object cause SwiftUI to reevaluate the observing view.
@MainActor
final class CounterViewModel: ObservableObject {
@Published private(set) var count = 0
func incrementCounter() {
count += 1
}
}
struct CounterView: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
CounterView receives its view model through its initializer. This makes @ObservedObject the correct choice since the view does not control the object’s lifetime.
What is a @StateObject?
The @StateObject property wrapper creates persistent storage for an ObservableObject. Use it when the view creates the object itself:
struct CounterContainerView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
CounterView(viewModel: viewModel)
}
}
SwiftUI can recreate the CounterContainerView value many times, but it preserves the state object associated with the view’s identity. The same CounterViewModel instance remains available across those updates.
Marking the property as private also communicates ownership. A parent can’t accidentally inject another initial value that SwiftUI would ignore later.
Why an inline @ObservedObject resets
You might expect the following counter to preserve its view model. However, the view creates an object inline using @ObservedObject, which does not provide storage for the object’s lifetime:
struct ResettingCounterView: View {
@ObservedObject private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
struct RandomNumberView: View {
@State private var randomNumber = 0
var body: some View {
VStack {
Text("Random number is: \(randomNumber)")
Button("Randomize number") {
randomNumber = Int.random(in: 0..<1000)
}
ResettingCounterView()
}
}
}
Changing the random number updates the parent view. SwiftUI can recreate the ResettingCounterView value and replace its inline CounterViewModel, causing the counter to reset.
You can fix the reset by changing the property wrapper to @StateObject:
@StateObject private var viewModel = CounterViewModel()
SwiftUI will now preserve the view model for the lifetime of the view’s identity. Alternatively, let the parent own the state object and inject it into a child using @ObservedObject.
StateObject vs. ObservableObject
StateObject and ObservableObject are not alternatives. ObservableObject is the protocol adopted by your model, while @StateObject is the property wrapper a SwiftUI view uses to own that model.
In other words, your model conforms to ObservableObject, and the view chooses between @StateObject and @ObservedObject based on ownership.
What about @Observable?
For new code targeting iOS 17 and later, I recommend using the Observation framework instead. You can replace ObservableObject, @Published, and @StateObject with @Observable and @State:
import Observation
import SwiftUI
@Observable
@MainActor
final class CounterViewModel {
private(set) var count = 0
func incrementCounter() {
count += 1
}
}
struct CounterView: View {
@State private var viewModel = CounterViewModel()
var body: some View {
Text("Count is: \(viewModel.count)")
}
}
An injected @Observable model usually requires no property wrapper. Add @Bindable only when the child view needs bindings to the model’s properties.
@StateObject and @ObservedObject remain relevant when supporting older operating systems or working with existing ObservableObject models. Read my @Observable migration guide for the complete modern implementation.
When should you use each property wrapper?
Use the following rules when working with ObservableObject:
- The view creates the object: use
@StateObject private var. - The view receives the object: use
@ObservedObject var. - Several sibling views observe one object: let their parent own it and inject the same instance.
- New code targeting iOS 17 or later: consider
@Observablewith@Stateinstead.
I prefer making ownership visible in the declaration. A private state object clearly belongs to the view, while an observed object appears in the view’s initializer as a dependency.
Conclusion
@StateObject and @ObservedObject both observe changes, but only @StateObject owns the model’s lifetime. Use a state object when the view creates the instance and an observed object when the instance is injected.
For modern projects targeting iOS 17 and later, consider migrating to @Observable. The same ownership rule still applies, but you’ll use @State for an owned model and a regular or @Bindable property for an injected model.
If you like to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!