Skip to content
intermediate Phase 4 · Navigation & Data Flow

Observation Framework

Use @Observable macro, observation tracking, and the new Swift Observation framework.

45m
2 problems
Topic Progress 0%

@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 Observable protocol
  • 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:

  1. Import Observation instead of Combine
  2. Add @Observable macro
  3. Remove ObservableObject conformance
  4. 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+?

Question 1 options

2. How does @Observable differ from ObservableObject in terms of view updates?

Question 2 options

3. Which property wrapper prevents a property from being tracked in @Observable?

Question 3 options

4. What replaces @StateObject when migrating to @Observable?

Question 4 options

Flashcards

Question

What does @Observable macro generate?

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?

Answer

Use @ObservationIgnored to exclude the property from tracking.

Question

What replaces @EnvironmentObject with @Observable?

Answer

@Environment(Type.self) replaces @EnvironmentObject for injecting observable objects.

Question

What is the minimum iOS version for @Observable?

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.