← All courses

Observable Objects & Shared State

🗓 May 31, 2026 ⏱ 3 min read

When @State isn’t enough

@State is perfect for simple values owned by one view. But real apps have data shared across many screens — a shopping cart, the logged-in user, app settings — often backed by logic and network calls. For that, you move the data into a reference type (a class) that SwiftUI can observe.

Making a class observable

Mark properties that should trigger UI updates with @Published inside an ObservableObject. When a published property changes, any view watching the object refreshes.

class Cart: ObservableObject {
    @Published var items: [String] = []

    func add(_ item: String) {
        items.append(item)      // views watching 'items' will update
    }
}

(On the newest iOS versions, the simpler @Observable macro replaces ObservableObject + @Published, but the idea is identical: mark a class as observable and views react to its changes.)

@StateObject vs @ObservedObject

This is a crucial distinction:

  • @StateObject — use where the object is created and owned. SwiftUI keeps it alive across redraws. Create it here, once.
  • @ObservedObject — use when an object is passed in from somewhere else. The view watches it but doesn’t own it.
struct CartScreen: View {
    @StateObject private var cart = Cart()   // owns it
    var body: some View {
        VStack {
            Text("\(cart.items.count) items")
            CartList(cart: cart)             // pass it down
        }
    }
}

struct CartList: View {
    @ObservedObject var cart: Cart           // receives it
    var body: some View {
        ForEach(cart.items, id: \.self) { Text($0) }
    }
}

Getting these backwards (using @ObservedObject where you should own it) can cause the object to be recreated and lose its data — a common, confusing bug.

@EnvironmentObject: app-wide data

For data many distant screens need (the current user, a theme), inject it once into the environment and read it anywhere below — no manual passing through every screen.

// inject at the top
ContentView().environmentObject(Cart())

// read it anywhere deep in the tree
struct Badge: View {
    @EnvironmentObject var cart: Cart
    var body: some View { Text("\(cart.items.count)") }
}

How to choose

  • Simple value, one view → @State.
  • Passed to a child for editing → @Binding.
  • Shared object you create/own → @StateObject.
  • Shared object passed in → @ObservedObject.
  • App-wide object many screens need → @EnvironmentObject.

Common mistakes

  • Using @ObservedObject where you create the object (it can get recreated and reset) — use @StateObject.
  • Forgetting @Published, so views don’t update.
  • Forgetting to inject an @EnvironmentObject (runtime crash).
Summary: Put shared data in an ObservableObject with @Published properties. Own it with @StateObject, receive it with @ObservedObject, and share app-wide data with @EnvironmentObject.