Stalk Your Data in SwiftUI 5 with Observable

Alessio Rubicini
5 min readNov 2, 2023

--

In one of my previous articles, I presented an overview of the most important SwiftUI property wrappers to manage your app’s data flow, observing your properties to trigger UI updates.

Things have changed recently, and Apple presented numerous new features for developers at WWDC23. Among these we find Observation, a new Swift feature for tracking changes in properties.

Here’s a detailed explanation on how to stalk your data with Observation.

What is Observation?

Observation is a new and simplified way to track changes in properties of our data models. The framework relies on Swift macros, described by Apple as it follows:

Swift macros help you avoid writing repetitive code in Swift by generating that part of your source code at compile time. Calling a macro is always additive: the macro adds new code alongside the code that you wrote, but never modifies or deletes code that’s already part of your project.

Let’s see the WWDC23 example from Discover Observation with SwiftUI:

@Observable class FoodTruckModel {
var orders: [Order] = []
var donuts = Donut.all
}

From our point of view, the Observable macro simply makes our class a type able to be observed for changes. What really happens under the hood is that the macro tells the Swift compiler to add code to our class that makes it so.

If you are curious to see what happens in detail, you can expand the macro and read the added code simply by clicking on the code where you call the macro in Xcode, then choose Editor > Expand Macro.

Adding the macro is really simple and immediate. But the most awesome and exciting part is that we don’t need to use any property wrappers to take advantage of this for our SwiftUI views!

struct DonutMenu: View {
var model: FoodTruckModel

var body: some View {
List {
Section("Donuts") {
ForEach(model.donuts) { donut in
Text(donut.name)
}
Button("Add new donut") {
model.addDonut()
}
}
}
}
}

As mentioned previously, the macro is additive. The Observable macro expands your types so they can be observed for changes. This lets SwiftUI track access to the properties and observe when the next property will change out of that Observation.

Previously, updating an ObservableObject risked causing all views observing that object via @StateObject or @ObservedObject wrappers to update accordingly. With Observable, SwiftUI views update only when a property of interest to them changes, which leads to great performance improvements.

What about our dear old property wrappers?

Although Observable makes our life easier, we can’t forget the good old property wrappers.

State, Environment and Bindable are now the three primary property wrappers for working with SwiftUI:

  • you use @State when the view needs to have its own state stored in a model in a local context.
  • Environment lets values be propagated as globally accessible values. This lets things be shared in many places across your app.
  • @Bindable is the newest of the family, and all it does is allow bindings to be created from that type. Most often, this will be bindings to observable types.
@Observable
class Book: Identifiable {
var title = "Sample Book Title"
var isAvailable = true
}


struct BookEditView: View {
@Bindable var book: Book

var body: some View {
Form {
TextField("Title", text: $book.title)
Toggle("Book is available", isOn: $book.isAvailable)
}
}
}

Simplifying down to the three primary property wrappers @State, @Environment and @Bindable makes writing new features easier to reason about since there are fewer options that need to be considered.

Here’s Apple’s summary:

Source: Discover Observation in SwiftUI (Apple)

Observable and computed properties

Observable lets you build your models how you want. You can have arrays of models being observed, or model types that contain other observable model types, like a matryoshka. The general rule is (for Observable) if a property that is used changes, the view will update.

But there’s an exception to this rule here. When a computed property lacks any associated stored properties, we need two additional steps to enable it for observation. This situation arises only when the property to be observed isn’t influenced by the composition of stored properties within the observable type. In such cases, the only necessary actions involve notifying Observation when the property is accessed and when it undergoes changes.

To address this, we manually redefine the custom access points, allowing us to read from and store data in non-observable locations.

@Observable class Donut {
var name: String {
get {
access(keyPath: \.name)
return someNonObservableLocation.name
}
set {
withMutation(keyPath: \.name) {
someNonObservableLocation.name = newValue
}
}
}

Typically, these manual adjustments are unnecessary because most model properties are usually derived from other stored properties. However, in rare instances where advanced functionality is required, Observation offers the flexibility and ease to accomplish this task.

SwiftUI can automatically detect changes in composition by monitoring observable types through property access. This means that if a computed property relies on other stored properties, Observation will function smoothly. Nonetheless, for the exceptional scenarios where this doesn’t apply, you have the option to employ Observation directly to explicitly flag property access and modification.

From ObservableObject to Observable

If you have been using SwiftUI for a while, you will surely have apps that use the Observable Object protocol to manage properties observation.

To switch to the new Observable framework and make the most of all its advantages, there are very few changes to make.

This is the old version with ObservableObject:

public class FoodTruckModel: ObservableObject {
@Published public var truck = Truck()
@Published public var orders: [Order] = []
@Published public var donuts = Donut.all

...
}

struct AccountView: View {
@ObservedObject var model: FoodTruckModel
@EnvironmentObject private var accountStore: AccountStore
...
}

And this is the new one with Observable:

@Observable public class FoodTruckModel {
public var truck = Truck()
public var orders: [Order] = []
public var donuts = Donut.all

...
}

struct AccountView: View {
var model: FoodTruckModel
@Environment private var accountStore: AccountStore
...
}
  1. The conformance to the ObservableObject protocol is replaced by the @Observable macro.
  2. The @Published property wrapper on the various class properties disappears.
  3. The @ObservedObject property wrapper disappears, while @EnvironmentObject just turns into @Environment.

Conclusion

Observable is a truly powerful tool that will make it easier to develop well-structured and optimized SwiftUI apps. I’m happy to see how Apple is improving the framework year by year adding new features or improving existing ones.

We’re on a path that will take us towards a stable, secure and self-sufficient SwiftUI.

--

--

Alessio Rubicini

Computer Science student at University of Camerino. Apple developer, in love with Swift.