Observable Objects & Shared State
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
@ObservedObjectwhere 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 anObservableObjectwith@Publishedproperties. Own it with@StateObject, receive it with@ObservedObject, and share app-wide data with@EnvironmentObject.