← All shorts
01iOS 17+2 min read

@Observable: State Without the Boilerplate

The @Observable macro replaces @Published and ObservableObject with less code and smarter view invalidation.

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 body changes, 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.

swiftuiios17state

Related shorts