ObservableObject makes you tag every stored property with @Published, and any change to any of them invalidates every view that observes the object — even a view that only reads one unrelated property.
#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 on every property — the macro instruments the whole class at compile time
- Views only redraw 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 only tracks properties that are read directly during a view's body evaluation. If you capture a property once — say, inside a closure stored at init time, or a value pulled out into a let before body runs — SwiftUI never registers that access, so the view won't invalidate when the property later changes. This trips people up most often in list rows and custom container views where the "read" happens once during setup rather than every render. Also worth knowing: properties you don't want tracked (large caches, computed helpers) need @ObservationIgnored, or every read of them will register as a dependency too.