SwiftUI Patterns

@StateObject vs @ObservedObject

June 27, 2026
5 min read
Featured image for blog post: @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 WrapperUse When
@StateObjectThis view creates and owns the object
@ObservedObjectThis 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

  1. Does this view create the object? → @StateObject
  2. Does this view receive it as a parameter? → @ObservedObject
  3. 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.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch