@StateObject vs @ObservedObject

When do you use @StateObject vs @ObservedObject? The difference is ownership. Here's how to choose.
Follow along with the code: iOS-Practice on GitHub
The Rule
| Property Wrapper | Use When |
|---|---|
@StateObject | This view creates and owns the object |
@ObservedObject | This view receives the object from elsewhere |
@StateObject: You Create It
struct ParentView: View {
@StateObject private var cartManager = CartManager()
var body: some View {
ChildView()
.environmentObject(cartManager)
}
}
The parent creates CartManager. Use @StateObject.
@ObservedObject: You Receive It
struct ChildView: View {
@ObservedObject var cartManager: CartManager
var body: some View {
Text("Items: \(cartManager.items.count)")
}
}
The child receives CartManager from its parent. Use @ObservedObject.
Why It Matters
@StateObject survives view re-creation. @ObservedObject doesn't.
// WRONG: Will reset on every view update
struct ProductList: View {
@ObservedObject var viewModel = ProductViewModel() // Creates new instance each time!
}
// CORRECT: Persists across view updates
struct ProductList: View {
@StateObject private var viewModel = ProductViewModel()
}
EnvironmentObject: Implicit Passing
For objects that many views need:
// Parent injects
.environmentObject(cartManager)
// Any descendant accesses
@EnvironmentObject var cartManager: CartManager
Shopping Cart Example
class CartManager: ObservableObject {
@Published var items: [CartItem] = []
var totalItems: Int {
items.reduce(0) { $0 + $1.quantity }
}
func addToCart(_ product: Product) {
if let index = items.firstIndex(where: { $0.product.id == product.id }) {
items[index].quantity += 1
} else {
items.append(CartItem(product: product, quantity: 1))
}
}
}
// Root view creates and injects
struct ShopView: View {
@StateObject private var cartManager = CartManager()
var body: some View {
NavigationStack {
ProductList()
.environmentObject(cartManager)
}
}
}
// Child views access via environment
struct ProductRow: View {
let product: Product
@EnvironmentObject var cartManager: CartManager
var body: some View {
HStack {
Text(product.name)
Button("Add") {
cartManager.addToCart(product)
}
}
}
}
Cart Badge Pattern
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showCart = true
} label: {
ZStack(alignment: .topTrailing) {
Image(systemName: "cart")
if cartManager.totalItems > 0 {
Text("\(cartManager.totalItems)")
.font(.caption2)
.fontWeight(.bold)
.foregroundColor(.white)
.padding(4)
.background(Color.red)
.clipShape(Circle())
.offset(x: 8, y: -8)
}
}
}
}
}
Decision Flowchart
- Does this view create the object? →
@StateObject - Does this view receive it as a parameter? →
@ObservedObject - Does this view get it from the environment? →
@EnvironmentObject
Interview Tip
A common mistake is using @ObservedObject where @StateObject is needed, causing unexpected resets. Explain the ownership difference.