With ObservableObject, every stored property needs @Published. Change one and every view observing the object invalidates. That includes the view that reads items and never touches discountCode.
#The old way
import Combine
final class CartViewModel: ObservableObject {
@Published var items: [String] = []
@Published var discountCode: String = ""
@Published var isCheckingOut = false
}
struct CartView: View {
@StateObject private var viewModel = CartViewModel()
var body: some View {
List(viewModel.items, id: \.self) { item in
Text(item)
}
}
}
#The new way
import SwiftUI
import Observation
@Observable
final class CartViewModel {
var items: [String] = []
var discountCode: String = ""
var isCheckingOut = false
}
struct CartView: View {
@State private var viewModel = CartViewModel()
var body: some View {
List(viewModel.items, id: \.self) { item in
Text(item)
}
}
}
#Why it matters
- No @Published anywhere. The macro instruments the whole class at compile time
- Views redraw only when a property they actually read in
bodychanges, not on every mutation to the object - Plain @State replaces @StateObject. One less property wrapper to reason about
- No Combine import required just to get change notifications
#Gotcha
@Observable tracks a property only when a view reads it during body evaluation.
Capture that property anywhere else and tracking never happens. A closure stored at init time, a value pulled into a let before body runs: SwiftUI never registers the access, so the view stays put when the property changes later. List rows and custom container views are where this bites hardest, because the read happens once during setup rather than every render.
The reverse case costs you too. Properties you don't want tracked (large caches, computed helpers) need @ObservationIgnored, or every read of them registers as a dependency.