Perception - 对 @Observable 的向后兼容 [译]
Swift 5.9 为这门语言引入了强大的观测工具,但遗憾的是,这些工具仅在 iOS 17、macOS 14、tvOS 17、watchOS 10 及更新版本上可用。然而,据统计,不到 50% 的设备升级到了 iOS 17,这意味着多数开发者还需要等上几年才能使用这些工具。
因此,我们对这些观测工具进行了回溯兼容处理,使它们可以在早至 iOS 13、macOS 10.15、tvOS 13、watchOS 6 的 Apple 平台上运行,并以开源库的形式发布。这意味着你现在就可以借助我们的库,大幅简化你的 SwiftUI 视图。
来看看我们的新库 Perception 吧。
如何使用 Perception
这个库提供了 Swift 5.9 中观测工具的独立版本,可以在旧版 Apple 平台上使用。在设计你的模型时,使用我们的 @Perceptible
宏,而不是 @Observable
宏:
1
2
3
4
5
6
7
8
+import Perception
-@Observable
+@Perceptible
class FeatureModel {
var count = 0
}
这样,FeatureModel
类就能够追踪其属性的访问,并在这些属性发生变化时进行广播。
这个模型可以在 SwiftUI 视图中使用,但还需要额外一步来确保视图订阅了模型的变化。我们必须使用一个名为 WithPerceptionTracking
的特殊视图来包装你的视图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct FeatureView: View {
let model: FeatureModel
var body: some View {
+ WithPerceptionTracking {
Form {
Text(model.count.description)
Button("Increment") {
model.count += 1
}
}
}
+ }
}
虽然不得不用这种方式包装视图有些遗憾,但另一方面,我们能够立即使用这些观测工具,而不必等待 iOS 17 广泛普及,这确实是个好消息。
如果你忘记使用 WithPerceptionTracking
,别担心,我们的库会提醒你。如果 @Perceptible
类的字段在视图中被访问,但并未使用 WithPerceptionTracking
包装,将触发一个运行时警告:
🟣 运行时警告:Perceptible 状态被访问但未进行跟踪。请通过
WithPerceptionTracking
视图包装你的视图,以跟踪状态变化。
这个警告会立即提醒你哪里设置不当,并以一种显著但不显眼的方式提醒你。要调试这个问题,只需在 Xcode 的问题导航器中展开警告(⌘5),然后点击堆栈帧,就能找到你的视图中哪里访问了状态但没有使用 WithPerceptionTracking
。
Perception 库的工作机制
新的观测框架是 Swift 开源项目的一部分,这意味着所有相关的源代码,包括 @Observable
宏的源代码,都是公开可获取的。因此,我们能够将这些代码复制到一个新项目中,并进行了一些小修改,以确保它们能够正常编译。
我们对这些代码做了一些重大改动,以适应我们的需求。首先,我们将所有涉及“观测”的命名改为“感知”(例如,@Observable
改为 @Perceptible
),以明确区分这些工具与 Apple 直接提供的 Swift 工具链中的工具。同时,我们也对所有回溯的工具进行了重命名和弃用,以便当你能将目标平台升级到 iOS 17 时,可以轻松地从我们的库迁移。
此外,我们希望我们的工具能在 iOS 17 设备上运行时,切换到 Apple 原生的观测框架。为此,我们在运行时做了一些额外的工作。所有工作都集中在 PerceptionRegistrar
上,它在可能的情况下会包装一个原生的 ObservationRegistrar
,如果不可能,则回退到我们的后备版本 _PerceptionRegistrar
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public struct PerceptionRegistrar: Sendable {
private let _rawValue: AnySendable
public init() {
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
#if canImport(Observation)
self._rawValue = AnySendable(ObservationRegistrar())
#else
self._rawValue = AnySendable(_PerceptionRegistrar())
#endif
} else {
self._rawValue = AnySendable(_PerceptionRegistrar())
}
}
}
我们还提供了方法,从这个类型中获取原始的 ObservationRegistrar
或 _PerceptionRegistrar
:
1
2
3
4
5
6
7
8
9
10
11
12
13
extension PerceptionRegistrar {
#if canImport(Observation)
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
private var registrar: ObservationRegistrar {
self._rawValue.base as! ObservationRegistrar
}
#endif
private var perceptionRegistrar: _PerceptionRegistrar {
self._rawValue.base as! _PerceptionRegistrar
}
}
需要注意的是,这些属性在技术上存在风险,因为我们使用了强制类型转换。但我们可以确保只在包装的值确实为我们期望的登记员类型时调用这些属性。
接下来,我们需要在 PerceptionRegistrar
上实现 access
、willSet
、didSet
和 withMutation
方法,并确保它们能够在运行时动态地选择使用我们的回溯工具或 Apple 的原生工具。
例如,access
方法专门用于处理回溯的 Perceptible
类型,而不是 Observable
类型:
1
2
3
4
5
6
7
8
9
extension PerceptionRegistrar {
public func access<Subject: Perceptible, Member>(
_ subject: Subject,
keyPath: KeyPath<Subject, Member>
) {
…
}
}
在这个方法的实现中,我们会动态检查 iOS 17 是否可用,如果可用,则尝试将对象转换为 Observable
协议,并采取一些技巧来打开类型封装并将键路径转换为相应的类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extension PerceptionRegistrar {
@_disfavoredOverload
public func access<Subject: Perceptible, Member>(
_ subject: Subject,
keyPath: KeyPath<Subject, Member>
) {
#if canImport(Observation)
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
func `open`<T: Observable>(_ subject: T) {
self.registrar.access(
subject,
keyPath: unsafeDowncast(keyPath, to: KeyPath<T, Member>.self)
)
}
if let subject = subject as? any Observable {
open(subject)
}
} else {
perceptionCheck()
self.perceptionRegistrar.access(subject, keyPath: keyPath)
}
#endif
}
}
通过这些技巧,iOS 16 及更早版本的设备可以使用我们的感知框架,而 iOS 17 及更新版本的设备则会使用 Swift 5.9 中的原生观测工具。
立即开始体验
现在就在你的项目中试用 Perception,即使你还无法针对最新的 Apple 平台,也能开始体验 Swift 强大的观测工具。
原文作者:Point-Free
原文链接:https://www.pointfree.co/blog/posts/129-perception-a-back-port-of-observable