Oskar Groth
Oskar GrothSeptember 20, 2023

Controlling SwiftUI View updates with Observation

Learn how to use the new Observation framework introduced at WWDC 2023 to ensure the best performance for your SwiftUI app.
Observation in SwiftUI

ObservableObject: An unwieldy approach

One of the major selling points of SwiftUI is the declarative approach to user interface design. By composing a hierarchy of views that depend on data models, SwiftUI automatically updates the affected parts of your interface without the need for explicit updates.

However, one of the biggest problems with SwiftUI has been with the fact that it does not know when a View actually depends on the updated value or not. Take this example:

class User: ObservableObject {
	@Published var age: Int
	@Published var name: String
	@Published var street: String
}
 
struct AgePicker: View {
    @ObservedObject var user: User
    var body: some View {
        Slider(value: $user.age, label: { Text("Age") })
    }
}
 
struct StreetPicker: View {
    @ObservedObject var user: User
    var body: some View {
        TextField("Street", text: $user.street)
    }
}

Now, if we add a Self._printChanges() in the StreetPicker body, we will see that it triggers when we drag the AgePicker slider.

Hold on, what now? Why does changing age trigger StreetPicker to refresh?

Because ObservableObject works by emitting a objectWillChange() notifier to the view when any @Published value inside it changes.

This mechanic is a bit counterintuitive, because it means that because we change one variable in User, every view observing a User will rerun its body regardless if the change was relevant to that particular view.

It is also pretty bad news for a complex SwiftUI app. If we end up observing an object from many different views, our whole app could exhibit performance issues when dragging a simple Slider to change a value that we only care about in one small part of the app.

Sensei Monitor

We wrote Sensei Monitor in SwiftUI by taking great care to ensure that one data change does not force unnecessary refresh cycles. As a result, it's now the best performing menu bar System Monitor available for the Mac.

New @Observable Macro

At WWDC 2023, Apple adressed this issue by introducing the new @Observable and @Bindable macros as part of the new Observation framework. The new @Observable macro replaces the conformance to ObservableObject and @Published annotations for the values in our model, and the new @Bindable property wrapper ensures that we can create bindings to the values.

But what is more important is that @Observable introduces a new mechanic for driving UI updates: it can automatically detect whether an update actually affects the view from which you are observing.

Let's see how adopting Observable would look for our model and views:

import Observation // Observable is imported separate from SwiftUI
 
@Observable class User {
    var age: Int
    var name: String
    var street: String
}
 
struct AgePicker: View {
    @Bindable var user: User
    var body: some View {
        Slider(value: $user.age, label: { Text("Age") })
    }
}
 
struct StreetPicker: View {
    @Bindable var user: User
    var body: some View {
        TextField("Street", text: $user.street)
    }
}

This is already a bit cleaner, but most importantly, when we add the same Self._printChanges() in the new StreetPicker body, we will see that it no longer triggers when dragging the age slider!


In conclusion, this is a very critical improvement to SwiftUI and will probably resolve a lot of performance issues that developers have encountered while trying to implement SwiftUI in real-world apps.

Unfortunately, the new Observation framework requires iOS 17.0 / macOS 14.0 or later. This means that it will be a few years before most developers can benefit from these changes.

If you need to support older OS and want to learn strategies for constraining view updates in SwiftUI without using Observation, stay tuned for our next post on this topic!

Check out the demo for this example on our GitHub: https://github.com/Cindori/ObservableTests