SwiftUI’s rendering system is built around a single core idea: views are cheap to recreate, but state must be preserved. To support this model, SwiftUI needs a reliable way to observe changes in data and update only the parts of the UI that depend on those changes.
Prior to iOS 17, this responsibility was handled primarily by ObservableObject and @Published. While effective, that approach introduced boilerplate, relied heavily on Combine, and triggered view updates at the object level rather than the property level.
With iOS 17, Apple introduced the Observation framework, centered around the @Observable macro. This new system provides a lighter, more precise way to model observable state in SwiftUI.
This article explains how the Observation framework works, how to use @Observable correctly in SwiftUI, how it differs from ObservableObject, and how to migrate existing code safely.
Before iOS 17, observable state in SwiftUI typically looked like this:
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var isPremium: Bool = false
}This approach worked, but it had several drawbacks:
Boilerplate everywhere:
Every observable type had to conform to ObservableObject, and every changing property required @Published.
Over-notification:
A change to any @Published property invalidates all dependent views, even if they don’t use that property.
Manual correctness:
Forgetting @Published silently breaks UI updates.
Apple’s answer to these problems is the Observation framework, which introduces a compiler-driven observation system.
@ObservableAt the core of the new system is the @Observable macro.
@Observable
class User {
var name: String = ""
var isPremium: Bool = false
}That’s it.
No ObservableObject.
No @Published.
Every stored property becomes observable by default.
Apple designed @Observable with three principles in mind:
Explicit at the type level: You mark what is observable, not how each property behaves.
Fine-grained dependency tracking: Views only re-render when the specific properties they read change.
Minimal runtime overhead: Observation metadata is generated at compile time.
This is where the new system truly shines.
With @Observable, SwiftUI tracks which properties are read during view evaluation.
struct ProfileView: View {
@State private var user = User()
var body: some View {
Text(user.name)
}
}In this example:
ProfileView depends on user.nameuser.isPremium does not invalidate this viewname trigger a re-renderThis is a major improvement over ObservableObject, where any @Published change triggers objectWillChange.
SwiftUI view structs are ephemeral and may be recreated frequently. For this reason, observable models must be initialized in stable storage.
struct SettingsView: View {
private let settings = InvoiceSettingsModel()
var body: some View {
Text(settings.currencyCode)
}
}This may appear correct, but the model can be recreated whenever the view is reinitialized.
@StateObservable models should be initialized using @State.
import SwiftUI
import Observation
@Observable
class InvoiceSettingsModel {
var currencyCode: String = "USD"
var includeTax: Bool = false
}
struct SettingsView: View {
@State private var model = InvoiceSettingsModel()
var body: some View {
Form {
Toggle("Include Tax", isOn: $model.includeTax)
Text("Currency: \(model.currencyCode)")
}
}
}Here:
@State guarantees stable model lifetime@Observable enables fine-grained updatesTo understand why an
@Observableclass must be initialized using@Statewithin the SwiftUI hierarchy, refer to this excellent article by Natalia Panferova:
Initializing @Observable classes within the SwiftUI hierarchy
Not every property should trigger UI updates.
@Observable
class AnalyticsModel {
var visibleCount = 0
@ObservationIgnored
var internalCache: [String: Int] = [:]
}Use @ObservationIgnored to exclude properties that:
ObservableObject)class ViewModel: ObservableObject {
@Published var title: String = ""
@Published var isLoading = false
}struct ContentView: View {
@ObservedObject var model = ViewModel()
var body: some View {
Text(model.title)
}
}@Observable)@Observable
class ViewModel {
var title: String = ""
var isLoading = false
}struct ContentView: View {
@State private var model = ViewModel()
var body: some View {
Text(model.title)
}
}| Aspect | ObservableObject |
@Observable |
|---|---|---|
| Boilerplate | High | Minimal |
| Property annotations | Required | Automatic |
| Dependency tracking | Object-wide | Property-level |
| Compile-time safety | Low | High |
Because dependencies are tracked at the property level:
This makes the Observation framework particularly useful for:
You don’t need to migrate everything at once.
ObservableObject models working@Observable for new features@Published usage@ObservedObject where possibleSwiftUI fully supports mixing both systems.
@Observable?Use it when:
Avoid it if:
The Observation framework is not just a syntactic improvement—it’s a fundamental shift in how SwiftUI tracks state changes.
By moving observation to the compiler level, Apple has made SwiftUI:
If you’re building modern SwiftUI apps targeting iOS 17 and later, @Observable should be your default choice for shared state.
Thank you for reading. If you have any questions feel free to follow me on X and send me a DM. If you enjoyed this article and would like to support me, Buy me a coffee.