The @Observable macro, introduced in Swift 5.9 and now the standard for iOS 17+/iOS 27, replaces ObservableObject/Published with a simpler, more performant observation system. SwiftUI views automatically track only the specific properties they read, reducing unnecessary re-renders.
⢠@Observable (Observation framework) is now the Apple-recommended default over ObservableObject for all new SwiftUI code in iOS 27 projects
⢠@Bindable replaces @ObservedObject for passed-in references requiring two-way binding
⢠SwiftUI tooling and Xcode templates default to @Observable patterns
⢠Combined with Swift concurrency @MainActor, @Observable models integrate cleanly with async/await data loading
⢠Eliminates boilerplate: no @Published on every property, no ObservableObject conformance, just @Observable on the class
⢠Fine-grained dependency tracking means views only re-render when properties they actually use change, not any property on the object
⢠Works seamlessly with @State, @Environment, and @Bindable ā no more @StateObject/@ObservedObject confusion
Demonstrates an @Observable view model driving a SwiftUI list, including a child view using @Bindable for two-way binding to a cart item.
import SwiftUIāimport Combine+import Observationāclass CartItemLegacy: ObservableObject, Identifiable {+@Observable+final class CartItem: Identifiable {let id = UUID()ā @Published var name: Stringā @Published var quantity: Intā @Published var pricePerUnit: Double+ var name: String+ var quantity: Int+ var pricePerUnit: Doublevar totalPrice: Double { Double(quantity) * pricePerUnit }init(name: String, quantity: Int, pricePerUnit: Double) {self.name = nameself.quantity = quantityself.pricePerUnit = pricePerUnit}}āclass CartViewModelLegacy: ObservableObject {ā @Published var items: [CartItemLegacy] = [ā CartItemLegacy(name: "Apple", quantity: 3, pricePerUnit: 0.99),ā CartItemLegacy(name: "Bread", quantity: 1, pricePerUnit: 2.49),ā CartItemLegacy(name: "Milk", quantity: 2, pricePerUnit: 1.79)+@Observable+@MainActor+final class CartViewModel {+ var items: [CartItem] = [+ CartItem(name: "Apple", quantity: 3, pricePerUnit: 0.99),+ CartItem(name: "Bread", quantity: 1, pricePerUnit: 2.49),+ CartItem(name: "Milk", quantity: 2, pricePerUnit: 1.79)]var grandTotal: Double { items.reduce(0) { $0 + $1.totalPrice } }func removeItems(at offsets: IndexSet) {items.remove(atOffsets: offsets)}}āstruct CartItemRowLegacy: View {ā // Requires @ObservedObject ā cannot use simple property referenceā @ObservedObject var item: CartItemLegacy+struct CartItemRow: View {+ @Bindable var item: CartItemvar body: some View {HStack {Text(item.name).frame(maxWidth: .infinity, alignment: .leading)Stepper("\(item.quantity)", value: $item.quantity, in: 1...99)Text(String(format: "$%.2f", item.totalPrice)).frame(width: 60, alignment: .trailing).foregroundStyle(.secondary)}}}āstruct ShoppingCartViewLegacy: View {ā // Must use @StateObject, not @State, for reference typesā @StateObject private var cart = CartViewModelLegacy()+struct ShoppingCartView: View {+ @State private var cart = CartViewModel()var body: some View {NavigationStack {List {ForEach(cart.items) { item inā CartItemRowLegacy(item: item)+ CartItemRow(item: item)}.onDelete(perform: cart.removeItems)}.navigationTitle("Cart")+ .toolbar {+ ToolbarItem(placement: .bottomBar) {+ Text(String(format: "Total: $%.2f", cart.grandTotal))+ .font(.headline)+ }+ }}}+}++#Preview {+ ShoppingCartView()}
⢠Use @Bindable (not @ObservedObject) when you need two-way bindings to an @Observable model passed into a view ⢠@Observable classes do NOT use @Published ā adding it will cause a compiler error ⢠Stored properties must have initial values or be set in init; computed properties are observed automatically ⢠Cannot be used on structs ā only classes ⢠If you need to store an @Observable object in @State, it works correctly unlike the old pattern
None
More iOS 27 APIs land every week.
Get notified when new capabilities are published ā no noise, just signal.