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
Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35

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.

If you have older iOS apps and want to enhance them with modern SwiftUI features, check out my book Integrating SwiftUI into UIKit Apps. It provides detailed guidance on gradually adopting SwiftUI in your UIKit projects. Additionally, if you're eager to enhance your Swift programming skills, my latest book Swift Gems offers over a hundred advanced tips and techniques, including optimizing collections, handling strings, mastering asynchronous programming, and debugging, to take your Swift code to the next level.

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

Enhance older apps with SwiftUI!$45

A detailed guide on gradually adopting SwiftUI in UIKit projects

Updated for iOS 18 and Xcode 16!

Integrating SwiftUI into UIKit Appsby Natalia Panferova

  • Upgrade your apps with new features like Swift Charts and Widgets
  • Support older iOS versions with effective backward-compatible strategies
  • Seamlessly bridge state and data between UIKit and SwiftUI using the latest APIs

Enhance older apps with SwiftUI!

A detailed guide on gradually adopting SwiftUI in UIKit projects

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

Integrating SwiftUI
into UIKit Apps

by Natalia Panferova

Updated for iOS 18 and Xcode 16!

$45