Skip to main content

Using SwiftUI Observation Framework

1. Using @Observable Macro

The @Observable macro in SwiftUI is a powerful tool for creating observable models that automatically handle state updates and view refreshes. It simplifies the process of managing state in SwiftUI applications by providing a clean, declarative syntax.

Property Description Usage
@Observable Macro that makes a class observable, allowing SwiftUI to track changes and update views accordingly. @Observable is applied to a class declaration to make it observable.

Example: Basic Usage of @Observable

@Observable
class User {
    var name: String = "John Doe"
    var age: Int = 30
}

struct ContentView: View {
    @State private var user = User()
    
    var body: some View {
        Text("Name: \"\(user.name)\"")
            .onChange(of: user.name) { _, newValue in
                print("Name changed to: \"\(newValue)\"")
            }
    }
}
Note: The @Observable macro automatically generates the necessary didSet observers for properties, making it easier to track and respond to state changes in SwiftUI views.
Warning: When using @Observable, ensure that properties are not marked as private or fileprivate if they need to be accessed from SwiftUI views, as this can cause issues with view updates.

2. Understanding Observation vs Combine

Feature SwiftUI Observation Combine
Purpose Declarative data binding and state management Asynchronous programming and event handling
Primary Use Case UI updates in response to data changes Handling streams of data and events
Data Flow Automatic, built-in to SwiftUI Manual subscription and handling required
API Style Property wrappers like @State, @Binding, @ObservedObject Publisher/Subscriber pattern with Publishers and Subscribers
Complexity Simple and intuitive for basic use cases More complex, requires understanding of publishers and subscribers
Performance Optimized for SwiftUI's declarative nature More flexible but can be less optimized without careful design

Key Differences Summary

  • SwiftUI Observation: Designed specifically for SwiftUI, simplifies state management with property wrappers.
  • Combine: A more general-purpose framework for handling asynchronous data streams and events.

Example: Using @State in SwiftUI (Observation)

import SwiftUI

struct ContentView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

Example: Using Combine Publisher (Combine)

import SwiftUI
import Combine

class Counter: ObservableObject {
    @Published var count = 0
}

struct ContentView: View {
    @StateObject private var counter = Counter()

    var body: some View {
        VStack {
            Text("Count: \(counter.count)")
            Button("Increment") {
                counter.count += 1
            }
        }
    }
}

3. Using @Bindable for Two-Way Binding

Property Type Description
@Bindable Binding<T> Property wrapper for creating two-way bindings in SwiftUI

Example: Using @Bindable

import SwiftUI

class ViewModel: ObservableObject {
    @Published var name = "John"
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        TextField("Enter name", text: $viewModel.name)
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Note: The @Bindable property wrapper is used to create a binding to a value that can be modified by the view. It's particularly useful in SwiftUI for managing state and ensuring two-way data flow between views and their underlying data models.

4. Migrating from ObservableObject

ObservableObject vs @Observable

  • ObservableObject is a class-based approach using @Published properties and ObservableObject protocol.
  • @Observable is a new Swift macro-based approach that simplifies state management.
Feature ObservableObject @Observable
Declaration class ViewModel: ObservableObject { @Published var count = 0 } @Observable class ViewModel { var count = 0 }
Property Observation Requires @Published for each observable property No need for @Published, properties are automatically observed
State Updates Manual objectWillChange.send() or @Published trigger Automatic observation and updates
Usage in Views @ObservedObject or @StateObject @State or @Binding (no need for @ObservedObject)

Example: Migration from ObservableObject to @Observable

// Before: ObservableObject
class CounterViewModel: ObservableObject {
    @Published var count = 0

    func increment() {
        count += 1
    }
}

struct CounterView: View {
    @StateObject private var viewModel = CounterViewModel()

    var body: some View {
        Text("Count: \(viewModel.count)")
            .onTapGesture {
                viewModel.increment()
            }
    }
}

Example: Updated with @Observable

// After: @Observable
@Observable class CounterViewModel {
    var count = 0

    func increment() {
        count += 1
    }
}

struct CounterView: View {
    @State var viewModel = CounterViewModel()

    var body: some View {
        Text("Count: \(viewModel.count)")
            .onTapGesture {
                viewModel.increment()
            }
    }
}
Note: @Observable is available starting in Swift 6 and iOS 18. It provides a more concise way to manage state in SwiftUI views.

5. Using withObservationTracking

withObservationTracking is a function in SwiftUI that allows you to track observations made within a closure, which can be useful for debugging or understanding how SwiftUI's observation system works.

Parameter Type Description
content (() -> T) The closure to execute while tracking observations.
tracker (ObservationTracker) -> Void A closure that receives the tracker for inspection.

Example: Using withObservationTracking

import SwiftUI

@State private var count = 0
@State private var name = "Swift"

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Count: " + String(count))
            Text("Name: " + name)
            
            Button("Increment") {
                count += 1
            }
        }
        .onAppear {
            withObservationTracking({}) { tracker in
                print("Observations tracked: " + String(tracker.observed.count))
            }
        }
    }
}
Note: withObservationTracking is primarily useful for debugging and understanding how SwiftUI's observation system works. It does not affect the actual rendering or behavior of your views in a production environment.

6. Understanding Observation Dependencies

Dependency Type Description Example
@State Tracks changes to local state variables @State private var count = 0
@Binding Creates a two-way binding to a value from a parent view @Binding var isOn: Bool
@ObservedObject Observes changes in a class that conforms to ObservableObject @ObservedObject var user: User
@Published Property wrapper that publishes changes to an observable object @Published var name: String = ""
@Environment Accesses environment values provided by the view hierarchy @Environment(\.colorScheme) var colorScheme
@AppStorage Manages user preferences using UserDefaults @AppStorage("theme") var theme = "light"
Note: SwiftUI's observation system automatically tracks dependencies when you read a property wrapped with @State, @Binding, or other observable wrappers.

7. Combining Observation with SwiftUI

Example: Using @ObservedObject in SwiftUI

import SwiftUI

class User: ObservableObject {
    @Published var name = "John"
    @Published var age = 30
}

struct UserView: View {
    @ObservedObject var user: User
    
    var body: some View {
        VStack {
            Text("Name: " + user.name)
            Text("Age: " + String(user.age))
        }
    }
}

struct ContentView: View {
    let user = User()
    
    var body: some View {
        UserView(user: user)
    }
}
Property Type Description
@ObservedObject Property wrapper Binds a SwiftUI view to an observable object that can change
@StateObject Property wrapper Creates and owns an observable object that is initialized once
@Published Property wrapper Marks a property to notify observers when it changes
Note: @ObservedObject is used when the observable object is created outside of the view, while @StateObject is used when it's created inside the view.

Example: Using @StateObject

import SwiftUI

class Counter: ObservableObject {
    @Published var count = 0
}

struct CounterView: View {
    @StateObject private var counter = Counter()
    
    var body: some View {
        VStack {
            Text("Count: " + String(counter.count))
            Button("Increment") {
                counter.count += 1
            }
        }
    }
}

Comparison: @ObservedObject vs @StateObject

  • @ObservedObject: Used when the object is created outside the view
  • @StateObject: Used when the object is created inside the view and should be owned by it

8. Working with Optional Observations

Property Type Description
@ObservedObject Optional<ObservableObject> Used to observe an optional observable object. When the object is nil, no observation occurs.
@StateObject Optional<ObservableObject> Used to manage an optional observable object that is owned by the view. When nil, no state is managed.
@Published Optional<ObservableObject> Used to publish changes in an optional observable object, allowing SwiftUI to react to updates.

Example: Using Optional Observations

import SwiftUI

class OptionalViewModel: ObservableObject {
    @Published var data: String? = nil
}

struct ContentView: View {
    @StateObject private var viewModel = OptionalViewModel()
    
    var body: some View {
        VStack {
            if let data = viewModel.data {
                Text(data)
            } else {
                Text("No Data")
            }
        }
    }
}
Note: When using optional observations, ensure that the view properly handles nil states to avoid runtime errors.

9. Using @ObservationIgnored Property

The @ObservationIgnored property wrapper is used to mark properties that should be excluded from SwiftUI's automatic observation system. This is particularly useful when you have properties that are not relevant to the UI or when you want to prevent unnecessary view updates.

Property Description Usage
@ObservationIgnored Prevents a property from being observed by SwiftUI's observation system. Applied as a property wrapper to any property that should not trigger view updates.

Example: Using @ObservationIgnored

import SwiftUI

class DataModel: ObservableObject {
    @Published var name: String = "John"
    @ObservationIgnored var internalCounter: Int = 0
}

struct ContentView: View {
    @StateObject private var model = DataModel()
    
    var body: some View {
        Text("Name: \(<span class="property">model.name</span>)")
        Text("Counter: \(<span class="property">model.internalCounter</span>)")
    }
}
Note: The @ObservationIgnored property is not automatically updated when the property changes, and it does not trigger view updates. It's useful for internal state that should not affect the UI.

10. Optimizing Observation Performance

Approach Description Notes
@StateObject @StateObject var model = MyModel() Creates a new object and manages its lifecycle. Use for models that should not be recreated.
@ObservedObject @ObservedObject var model: MyModel Observes an object that is created elsewhere. Use for models passed down from parent views.
@State @State var value = "initial" Manages local state within a view. Efficient for simple values.
@Binding @Binding var value: String Creates a two-way binding to a value in a parent view. Use for passing values up and down the view hierarchy.

Example: Using @StateObject for Performance

struct ContentView: View {
    @StateObject private var userManager = UserManager()
    
    var body: some View {
        UserListView(users: userManager.users)
    }
}

Example: Using @ObservedObject for Performance

struct UserListView: View {
    @ObservedObject var users: UserManager
    
    var body: some View {
        ForEach(users.users) { user in
            UserRow(user: user)
        }
    }
}
Note: Avoid recreating objects in view bodies. Use @StateObject to create once and observe changes.
Warning: Using @State for large models can cause unnecessary re-renders. Prefer @StateObject or @ObservedObject.
Performance Tip Impact Implementation
Use @StateObject for root models Reduces unnecessary view rebuilds Initialize models with @StateObject at the top of your view hierarchy.
Avoid @State for large data structures Improves rendering performance Use @ObservedObject or @StateObject for collections or complex models.
Minimize view rebuilds Improves responsiveness Use @State for simple values and @Binding for two-way data flow.

Was this article helpful?