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

303 阅读4分钟

在这里插入图片描述

概述

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

在这里插入图片描述

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

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

  1. 适配 Swift 6 所经历的“荆棘载途”
  2. 鞭辟入里:问题的根源及解决

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

那还等什么呢?Let’s go!!!;)


4. 适配 Swift 6 所经历的“荆棘载途”

虽然上述实现在 Swift 5 的编译器中运行得相当不错,但如果我们尝试在 Swift 6 中编译它,实际上则会遇到一些尴尬的局面。

一旦我们选择 Swift 6 语言版本,立即会让编译器“怨声载道”:

func observe() {
  withObservationTracking { [weak self] in
    guard let self else { return }
    // Capture of'self' with non-sendable type 'CounterObserver?' in a `@Sendable` closure
    print("counter.count: \(counter.count)")
  }
}

这里的错误信息表明:withObservationTracking 方法要求我们传递一个 @Sendable 类型的闭包,这意味着我们不能捕获非 Sendable 属性。

然而,我们不能直接将闭包改为非 Sendable 类型,因为我们是在系统框架中 withObservationTracking 方法的 onChange 闭包中使用它的,而且正如大家可能猜到的那样,onChange 要求我们的闭包必须是 Sendable 类型的。

在大多数情况下,我们可以通过使用 @MainActor 修饰符来使得 self 成为 Sendable 对象。这样的话,对象就总是在主线程上进行其属性访问和方法调用了。

有时候这并不是一个坏主意,但当我们尝试在示例中应用它时,将会收到如下错误:

@MainActor
class CounterObserver {
  let counter: Counter

  init(counter: Counter) {
    self.counter = counter
  }

  func observe() {
    withObservationTracking { [weak self] in
      guard let self else { return }
      // Main actor-isolated property 'counter' can not be referenced from a Sendable closure
      print("counter.count: \(counter.count)")
    }
  }
}

此时,我们可以继续通过将属性访问操作包装(Wrap)在一个同样在主线程上运行的 Task 中来使代码编译通过,但这样做的结果是我们将以异步方式访问 counter,并且会丢失一些传入的事件。

那么,除了使用 @unchecked Sendable 来迫使 CounterObserver 成为 Sendable 类型以外:

class CounterObserver: @unchecked Sendable {...}

我们还有没有其它解决办法呢?

5. 鞭辟入里:问题的根源及解决

为了得出 Swift 6 中这个编译问题的根本原因,我们需要回过头来看看 withObservationTracking 方法的实际签名:

@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
public func withObservationTracking<T>(_ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void) -> T

从上面的方法签名可以看到:实际上 withObservationTracking 只有第二个闭包才有 Sendable 限定,而第一个完全不用遵守 Sendable 约定。

回忆一下我们重构后的辅助方法,@Sendable 闭包 execute 被 withObservationTracking 方法中的两个闭包先后调用了,这就是问题的根本之所在!

所以,解决此问题的一种方法是将调用拆开,完全跳过重构后的包装方法:

@MainActor
class CounterObserver {
  let counter: Counter

  init(counter: Counter) {
    self.counter = counter
  }

  func observe() {
      withObservationTracking {[weak self] in
          guard let self else { return }
          print("counter.count: \(counter.count)")
      } onChange: {
          DispatchQueue.main.async {
              self.observe()
          }
      }
  }
}

当然,在 onChange 闭包中我们也可以用 Task 来同样达到异步执行的目的:

func observe() {
    withObservationTracking {[weak self] in
        guard let self else { return }
        print("counter.count: \(counter.count)")
    } onChange: {
        Task {
            try? await Task.sleep(for: .seconds(0.01))
            await self.observe()
        }
    }
}

如果可以舍弃取消操作,我们甚至可以干脆用 actor 来实现整个 CounterObserver 类:

actor CounterObserver {
  let counter: Counter

  init(counter: Counter) {
    self.counter = counter
  }

  func observe() {
      withObservationTracking {
          print("counter.count: \(counter.count)")
      } onChange: {
          Task {
              try? await Task.sleep(for: .seconds(0.01))
              await self.observe()
          }
      }
  }
}

let counter = Counter()
let monitor = CounterObserver(counter: counter)

Task {
    await monitor.observe()
}

counter.count += 1

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    counter.count += 1
}

不过,这时必须 await monitor.observe() 方法的调用,所以自然我们会丢失一个 counter.count 属性初始化的监听事件了:

在这里插入图片描述

看到这里,小伙伴们是否对 Observation 框架以及 @Observable 可观察对象的监听有了更深刻的领悟呢?那还不赶紧给自己一个大大的赞,么么哒!

总结

在本篇博文中,我们讨论了之前 withObservationTracking 包装方法为何会在 Swift 6 的严格并发模式中被编译器“人怨神怒”,并最终给出解决方案。

感谢观赏,再会啦!8-)