概述
在 WWDC 24 中苹果推出了全新的 Observation 框架,借助于它我们可以更加细粒度的监听可观察(@Observable)对象 。同时,SwiftUI 自身也与时偕行开始全面支持 @Observable 对象的“嵌入”。
然而在这里,我们却另辟蹊径来介绍 @Observable 对象另外一些“鲜为人知”的故事。
在本篇博文中,您将学到如下内容:
- “独立自主”的 @Observable 对象
- 如何用 withObservationTracking 监听属性的 didSet 事件?
- 重构之后
相信学完本课后,小伙伴们一定会对“自立自强”的可观察对象的使用更加游刃有余、运用自如。
那还等什么呢?Let’s go!!!;)
1. “独立自主”的 @Observable 对象
Observation 框架是一个专注于观察我们活力四射、变化莫测 App 中所有可观察对象的。它在 iOS 17(macOS 14)中被引入,正好对应着 SwiftUI 5.0。
大家都知道,借助 SwiftUI,Observation 可以发挥出难以置信的巨大威力。不过除了和 SwiftUI “形影不离”以外,我们还可以让它雏鹰展翅独当一面。这是通过利用 withObservationTracking 方法做到的:
简单来说,@Observable 宏构建的可观察对象可以与 withObservationTracking 方法“琴瑟和鸣”:withObservationTracking 方法允许我们在闭包中跟踪可观察对象上所访问的属性。如果我们尝试访问的任何属性发生了变化,则该闭包将会被调用。
下面是具体的代码示例:
@Observable
class Counter {
var count = 0
}
class CounterObserver {
let counter: Counter
init(counter: Counter) {
self.counter = counter
}
func observe() {
withObservationTracking {
print("counter.count: \(counter.count)")
} onChange: {
self.observe()
}
}
}
如您所见:在 CounterObserver 中定义的 observe 方法里,我们访问了 counter 对象上的 count 属性。
该观察机制的工作原理是:我们在第一个闭包内访问的任何属性都会被标记为“感兴趣”。所以,如果这些属性中任何一个发生了变化(在这个例子中只有一个 count 属性),onChange 闭包就会被调用,以通知我们它们已经发生了改变。
我们可以这样测试观察的结果:
let counter = Counter()
let monitor = CounterObserver(counter: counter)
monitor.observe()
counter.count += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
counter.count += 1
}
运行结果如下所示:
counter.count: 0 counter.count: 0 counter.count: 1
从上面的输出结果,我们可以了解到三个事实:
- withObservationTracking 方法在调用后默认只会监听 1 次,如果需要持续监听我们需要反复调用它;
- withObservationTracking 方法能够监听到可观察对象属性的初始化;
- withObservationTracking 方法监听的是可观察对象属性的 willSet 事件;
上面输出的 3 行结果分别对应着 counter.count 属性在初始化、第一次和第二次更改这三个“美妙时刻”。因为 withObservationTracking 只能观察到 willSet 事件(即 onChange 闭包总是以 willSet 的语义来调用的),所以捕获到的总是“旧”值,这多少有点让人感到不便。
好奇的小伙伴们肯定会问:那我们该如何用 withObservationTracking 监听可观察(@Observable)对象的 didSet 事件呢?
答案简单的让你们想象不到!
2. 如何用 withObservationTracking 监听属性的 didSet 事件?
既然我们希望监听 willSet 之后那个“木已成舟”的 didSet 事件,我们只需要“异步”(async)在下一个“时间点”上重新设置监听即可。
代码超乎寻常的简单:
func observe() {
withObservationTracking {
print("counter.count: \(counter.count)")
} onChange: {
DispatchQueue.main.async {
self.observe()
}
}
}
如上代码所示:我们通过在 withObservationTracking 的 onChange 闭包里使用异步执行,再次调用了 observe 方法自身。
运行修改后的代码,可以看到我们已经如愿以偿监听到了“didSet 事件”:
3. 重构之后
上面的代码貌似不那么“雅观”,但它确实可行。不过我们还可以做的更好!
我们可以将上述代码封装到一个辅助函数中去,以减少重复(KISS 原则):
public func withObservationTracking(execute: @Sendable @escaping () -> Void) {
Observation.withObservationTracking {
execute()
} onChange: {
DispatchQueue.main.async {
withObservationTracking(execute: execute)
}
}
}
这使得我们可以在 observe() 方法中如此调用这个新的辅助方法了:
func observe() {
withObservationTracking { [weak self] in
guard let self else { return }
print("counter.count: \(counter.count)")
}
}
通过编写这个简单包装方法,现在我们可以向 withObservationTracking 传递一个单一的闭包。在这个闭包中我们所访问的任何属性,现在都会自动被观察其变化的情况,并且每当这些属性中的任何一个发生变化时,我们的闭包都会持续运行。
由于我们使用了弱引用(Weak Ref)来捕获 self,并且只有在 self 仍然存在时才会访问属性,所以我们也在一定程度上支持了某种形式的取消操作(Cancellation)。
需要注意的是:我们的实现方式与 Swift 官方开发者论坛上展示的实现还是有很大的不同,虽然它受到了论坛示例代码的启发,论坛上的实现上并不支持任何形式的取消操作。我们认为增加一些对取消操作的支持总比完全不支持要好。
在下一篇博文中,我们将继续介绍上述实现在 Swif 6 严格并发模式中所招致的“举步维艰”,不见不散。
总结
在本篇博文中,我们讨论了如何利用 withObservationTracking 方法独立监听可观察(@Observable)对象,并且介绍了如何捕获其中的 didSet 事件。
感谢观赏,我们下一篇见!8-)