Using Observation framework outside of SwiftUI

This year at WWDC 2023 Apple introduced the new Observation framework which provides an implementation of the observer design pattern in Swift. Classes marked with @Observable macro provided by the framework are now a better alternative to the older ObservableObject when combined with SwiftUI. The new observable classes can be used with existing data flow primitives like State and Environment and trigger view updates only when properties read in the view's body change. But we can also leverage the power of the Observation framework outside of SwiftUI, in plain Swift code and even in UIKit.

In this post we are going to see how to use withObservationTracking(_:onChange:) function included in the new Observation framework to track changes to properties of an observable class.

Let's say we have a User class that we marked with the @Observable macro. It has two properties: name and age.

@Observable class User {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

We'll make an example user called Jane and wish her a happy birthday when her age changes next time. To track changes to the age property, we need to read it inside the apply closure of withObservationTracking() function. The onChange parameter provides a closure to run when the value of age changes next time. Changes to other user properties, such as name will not trigger onChange code.

import Foundation
import Observation

let user = User(name: "Jane", age: 21)

_ = withObservationTracking {
    user.age
} onChange: {
    DispatchQueue.main.async {
        // Prints "Happy birthday! You are now 22!"
        print("Happy birthday! You are now \(user.age)!")
    }
}

user.age += 1

Note that I wrapped the print statement inside the onChange closure in DispatchQueue.main.async {}. I did this to print the new value of age which is 22. If we omit DispatchQueue.main.async {} and simply print the age, we will get the old value of age which is 21. So the onChange closure is called before new value is actually set.

let user = User(name: "Jane", age: 21)

_ = withObservationTracking {
    user.age
} onChange: {
    // Prints "Happy birthday! You are now 21!"
    print("Happy birthday! You are now \(user.age)!")
}

user.age += 1

Another thing to note is that onChange is only called once for the next update. If Jane's age keeps changing, with the current implementation we have, we'll still only congratulate her once.

let user = User(name: "Jane", age: 21)

_ = withObservationTracking {
    user.age
} onChange: {
    DispatchQueue.main.async {
        // Prints "Happy birthday! You are now 22!"
        print("Happy birthday! You are now \(user.age)!")
    }
}

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
    user.age += 1
}

This type of functionality means that we don't have to worry about manually cancelling updates. But it also means that we have to add a recursion if we do want to track subsequent changes.

We can do so by wrapping the code that implements the tracking into a separate function and then calling that function inside the onChange closure.

let user = User(name: "Jane", age: 21)

func confirmAge() {
    _ = withObservationTracking {
        user.age
    } onChange: {
        DispatchQueue.main.async {
            // Prints "Happy birthday! You are now 22!",
            // then "Happy birthday! You are now 23!" etc.
            print("Happy birthday! You are now \(user.age)!")
            confirmAge()
        }
    }
}

confirmAge()

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
    user.age += 1
}

We can use a similar implementation to track changes to an observable class and trigger UI updates in a UIKit view controller. That's really useful when bridging data between UIKit and SwiftUI parts of the same app. I wrote more about it with some examples in my book Integrating SwiftUI into UIKit Apps in the new chapter SwiftUI integration in iOS 17 that focuses on new iOS 17 APIs.

Note that we may experience some potential issues when using withObservationTracking() function that we should be aware of. Firstly, since there is no easy way to cancel the tracking, we have to make sure that we don't capture any strong references inside the onChange closure. Another point to keep in mind is that if we use asynchronous observation, there is no guarantee on the order of the actual executions, so we could be dealing with potentially un-ordered callbacks.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Check out our book!

Integrating SwiftUI into UIKit Apps

Integrating SwiftUI intoUIKit Apps

UPDATED FOR iOS 17!

A detailed guide on gradually adopting SwiftUI in UIKit projects.

  • Discover various ways to add SwiftUI views to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit