@Observable Macro
What is the Observation Framework?
Introduced in iOS 17, the Observation framework provides a modern, performance-optimized way to track state changes. The @Observable macro replaces the older ObservableObject protocol and @Published property wrapper pattern.
The key advantage is that @Observable only tracks properties that are actually read during a view's render, eliminating unnecessary view updates.
Creating an Observable Class
import Observation
@Observable
class UserProfile {
var name: String = ""
var email: String = ""
var avatarURL: URL?
var isPremium: Bool = false
func updateName(_ newName: String) {
name = newName
}
}
The @Observable macro automatically:
- Generates conformance to the
Observableprotocol - Creates storage for observation tracking
- Makes properties observable without
@Published
Using Observable Classes in SwiftUI
struct ProfileView: View {
var user: UserProfile
var body: some View {
VStack {
Text(user.name)
Text(user.email)
}
}
}
When user.name changes, only views that read name are updated. If ProfileView doesn't read email, it won't re-render when email changes.
Passing Observable Objects
Observable objects can be passed directly — no need for @StateObject or @ObservedObject:
struct ContentView: View {
@State var user = UserProfile()
var body: some View {
ProfileView(user: user)
}
}
Or use @Bindable for two-way bindings:
struct EditProfileView: View {
@Bindable var user: UserProfile
var body: some View {
TextField("Name", text: $user.name)
}
}
@Bindable vs @State
@State var user = UserProfile()— owns the instance@Bindable var user: UserProfile— binds to an existing instance (two-way)- Direct
var user: UserProfile— read-only access
Environment Integration
Observable objects work with the environment:
@Observable
class AppSettings {
var theme: Theme = .light
var notifications: Bool = true
}
// Inject into environment
ContentView()
.environment(AppSettings())
// Read in child view
struct ChildView: View {
@Environment(AppSettings.self) var settings
var body: some View {
Text(settings.theme.rawValue)
}
}
This replaces the need for @EnvironmentObject.
Observation Tracking
How Observation Tracking Works
The Observation framework uses a technique called "access tracking" to determine which properties a view reads during its body evaluation. Only changes to those specific properties trigger a view update.
This is more efficient than ObservableObject, which triggers updates for any @Published property change, even if the view doesn't use it.
Tracking Scope
Observation tracking works at the property level. Consider:
@Observable
class WeatherService {
var temperature: Double = 72.0
var humidity: Double = 45.0
var forecast: [Forecast] = []
}
If a view only reads temperature, it won't re-render when humidity or forecast change.
withObservationTracking
You can use withObservationTracking to manually observe changes:
let service = WeatherService()
withObservationTracking {
let temp = service.temperature
// This block tracks reads to service.temperature
} onChange: {
print("temperature changed!")
}
This is useful for testing or integrating with non-SwiftUI code.
Limitations of Access Tracking
Be aware of edge cases:
@Observable
class DataStore {
var items: [Item] = []
var totalCount: Int {
items.count
}
}
If a view reads totalCount, it's tracking items access (since totalCount reads items). Any change to items triggers a re-render.
Ignoring Properties
Use @ObservationIgnored to exclude properties from tracking:
@Observable
class NetworkManager {
var data: Data = Data()
@ObservationIgnored
var cachedResponse: Data?
@ObservationIgnored
var requestCount: Int = 0
}
Changes to cachedResponse or requestCount won't trigger SwiftUI updates.
Thread Safety
Observation tracking is not thread-safe by default. Mutations should occur on the main thread when the object is used in SwiftUI. Use @MainActor for thread safety:
@Observable
@MainActor
class UserProfile {
var name: String = ""
}
Migration from ObservableObject
Why Migrate?
ObservableObject (iOS 13+) uses Combine's PassthroughSubject for change notification. @Observable (iOS 17+) uses the Observation framework, which is more efficient because it only tracks properties actually read by views.
Migrating reduces unnecessary view updates and simplifies your code.
Step-by-Step Migration
Before (ObservableObject):
import Combine
class UserProfile: ObservableObject {
@Published var name: String = ""
@Published var email: String = ""
@Published var avatarURL: URL?
}
After (@Observable):
import Observation
@Observable
class UserProfile {
var name: String = ""
var email: String = ""
var avatarURL: URL?
}
The changes are minimal:
- Import Observation instead of Combine
- Add @Observable macro
- Remove ObservableObject conformance
- Remove @Published wrappers
Updating SwiftUI Views
Before:
struct ProfileView: View {
@StateObject var user = UserProfile()
var body: some View {
Text(user.name)
}
}
After:
struct ProfileView: View {
@State var user = UserProfile()
var body: some View {
Text(user.name)
}
}
Replace @StateObject with @State. Replace @ObservedObject with direct property passing. Replace @EnvironmentObject with @Environment(Type.self).
Handling @Published Equatable Checks
If you used @Published with custom equatable checks or removeDuplicates(), the Observation framework handles this automatically through property-level tracking.
Mixing ObservableObject and @Observable
During migration, you can mix both patterns. An @Observable class can be used with @ObservedObject for backward compatibility:
// In a view that needs both
struct MixedView: View {
@ObservedObject var legacyVM: LegacyViewModel
var newService: ModernService
var body: some View {
// Uses both
}
}
Backwards Compatibility
If you need to support iOS 16 and earlier, keep ObservableObject. @Observable requires iOS 17+. Use #available checks for conditional usage.
Performance Impact
Migrating typically reduces the number of view updates by 30-60%, because views only re-render when the specific properties they read change, not when any @Published property in the object changes.
Quiz
1. What macro creates an observable class in iOS 17+?
2. How does @Observable differ from ObservableObject in terms of view updates?
3. Which property wrapper prevents a property from being tracked in @Observable?
4. What replaces @StateObject when migrating to @Observable?
Flashcards
Question
What does @Observable macro generate?
Click to reveal answer
Answer
It generates Observable protocol conformance, observation tracking storage, and makes properties observable without @Published.
Question
How do you prevent a property from triggering view updates in @Observable?
Click to reveal answer
Answer
Use @ObservationIgnored to exclude the property from tracking.
Question
What replaces @EnvironmentObject with @Observable?
Click to reveal answer
Answer
@Environment(Type.self) replaces @EnvironmentObject for injecting observable objects.
Question
What is the minimum iOS version for @Observable?
Click to reveal answer
Answer
iOS 17.0. Use ObservableObject for iOS 16 and earlier.
Revision Notes
Key Takeaways
- 1. @Observable provides property-level tracking for efficient SwiftUI updates
- 2. Remove @Published wrappers when migrating from ObservableObject
- 3. @State replaces @StateObject, @Bindable replaces @ObservedObject
- 4. @ObservationIgnored excludes properties from triggering view updates
- 5. @Observable requires iOS 17+; keep ObservableObject for backward compatibility
Interview Tips
- • Explain how property-level tracking improves performance over ObservableObject
- • Know the migration steps: @Observable, remove @Published, update property wrappers
- • Describe when to use @ObservationIgnored for caching or derived state
- • Discuss backwards compatibility strategies for iOS 16 support
Cheat Sheet
@Observable macro (iOS 17+): Replaces ObservableObject + @Published.
Key differences:
- Property-level tracking (only reads matter)
- No @Published needed
- @State replaces @StateObject
- @Bindable replaces @ObservedObject for two-way binding
- @Environment(Type.self) replaces @EnvironmentObject
@ObservationIgnored: Excludes properties from tracking.
withObservationTracking: Manual change observation.
Thread safety: Use @MainActor for main-thread mutations.