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
@Publishedproperties andObservableObjectprotocol. - @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. |