← 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.

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 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 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.

swiftuiios17state

Related shorts