Greetings, traveler!
Apple introduced the Observation framework during WWDC 2023 to replace ObservableObject
via the new Observable
macro. This framework allows us to redraw SwiftUI views after applying changes to an Observable
class. But what if we want to track these changes outside of SwiftUI views? There is a way to do this. Looking ahead, I want to say that this API currently seems suspicious. I will explain why this is the case at the end of this article.
withObservationTracking
You can use the withObservationTracking
method for such a tracking. Check out this example:
import Observation
import Foundation
@Observable
final class Model {
var name: String
init(name: String) {
self.name = name
observe()
}
private func observe() {
withObservationTracking {
_ = name
} onChange: { [weak self] in
print(self?.name)
}
}
}
We have an Observable Model and want to track changes in its name property inside this class. We can do it with withObservationTracking
method.
let model = Model(name: "Kate")
model.name = "Jack"
Kate
But we printed the old value. What’s wrong? Since the onChange
closure was called before the “Jack” value was set, we just got the old value of the name property. To fix this behavior, we can wrap our code inside the DispatchQueue.main.async
.
private func observe() {
withObservationTracking {
_ = name
} onChange: {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
print(name)
}
}
}
Jack
Alright then, let’s try something like this:
struct SwiftUIView: View {
@State private var model: Model
private var names = ["John", "Kate", "Paul"]
init(model: Model) {
self.model = model
}
var body: some View {
Button {
model.name = names[Int.random(in: .zero...names.count - 1)]
} label: {
Text("Tap Me")
}
}
}
We have the SwiftUI View, which can trigger changes in its model. Let’s tap the button several times.
Paul
As we can see in the console, our tracking worked only once. The second button tap has no effect. Due to the nature of this API, changes will only trigger the print()
method once, so you must call withobservationtracking(_: on change :)
again to restart the observation. You can do this by adding a recursion to your method.
private func observe() {
withObservationTracking {
_ = name
} onChange: {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
print(name)
observe()
}
}
}
Now, it is working as expected.
Paul
John
John
Kate
John
John
Kate
Is it appropriate to use this method frequently?
You can use this method to track changes anywhere, even in your UIKit modules. But, as you can see, this approach is not very convenient. It looks like an Observable class was not intended to be used like that. If you will take a look at ObservationTracking.swift, you will notice that this method is annotated with @_spi(SwiftUI)
. This may indicate that this method was not meant for common use. Yes, this method was marked as a public one, but there is a feeling that this was done only because there was no other option. Therefore, this may mean that this part of the API may be affected by changes in the future.
If you enjoyed this article, please feel free to follow me on my social media: