Swift 进阶:Observation 框架中可观察(@Observable)对象的高级操作(上)

165 阅读5分钟

在这里插入图片描述

概述

在 WWDC 24 中苹果推出了全新的 Observation 框架,借助于它我们可以更加细粒度的监听可观察(@Observable)对象 。同时,SwiftUI 自身也与时偕行开始全面支持 @Observable 对象的“嵌入”。

在这里插入图片描述

然而在这里,我们却另辟蹊径来介绍 @Observable 对象另外一些“鲜为人知”的故事。

在本篇博文中,您将学到如下内容:

  1. “独立自主”的 @Observable 对象
  2. 如何用 withObservationTracking 监听属性的 didSet 事件?
  3. 重构之后

相信学完本课后,小伙伴们一定会对“自立自强”的可观察对象的使用更加游刃有余、运用自如。

那还等什么呢?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

从上面的输出结果,我们可以了解到三个事实:

  1. withObservationTracking 方法在调用后默认只会监听 1 次,如果需要持续监听我们需要反复调用它;
  2. withObservationTracking 方法能够监听到可观察对象属性的初始化;
  3. 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-)