Swift 5.9 一声炮响为我们带来全新的宏(Macro)机制,也同时带来了干霄凌云的 Observation 框架。
Observation 框架可以增强通用场景下的使用,也可以搭配 SwiftUI 5.0 而获得双剑合璧的更强威力。
那么,就让我们赶快进入 Observation 奇妙的世界吧!
Let‘s go!!!
简单来说,Observation 框架为我们提供了集鲁棒性(robust)、安全性、高性能等三大特性为一身的 Swift 全新观察者设计模式。
它的核心功能在于:监视对象状态,并在改变时做出反应!
在 Swift 5.9 中,我们可以非常轻松的通过 @Observable 宏将普通类“转化为”可观察(Observable)类。自然,它们的实例都是可观察的:
@Observable
final class Hero {
var name: String
var power: Int
init(name: String, power: Int) {
self.name = name
self.power = power
}
}
@Observable
final class Model {
var title: String
var createAt: Date?
var heros: [Hero]
init(title: String, heros: [Hero]) {
self.title = title
self.createAt = Date.now
self.heros = heros
}
}
如上代码所示,我们定义了两个可观察类 Model 和 Hero,就是这么简单!
在一个对象成为可观察之后,我们可以通过 withObservationTracking() 方法随时监听它状态的改变:
我们可以将对象需要监听的属性放在 withObservationTracking() 的 apply 闭包中,当且仅当( Hero 中其它属性的改变不予理会)这些属性发生改变时其 onChange 闭包将会被调用:
let hero = Hero(name: "大熊猫侯佩", power: 5)
func watching() {
withObservationTracking({
NSLog("力量参考值:\(hero.power)")
}, onChange: {
NSLog("改变之前的力量!:\(hero.power)")
watching()
})
}
watching()
hero.name = "地球熊猫"
hero.power = 11
hero.power = 121
以上代码输出如下:
使用 withObservationTracking() 方法有 3 点需要注意:
目前,上面测试代码在 Xcode 15 的 Playground 中编译会报错,提示如下:
error: test15.playground:8:13: error: external macro implementation type ‘ObservationMacros.ObservableMacro’ could not be found for macro ‘Observable()’
final class Hero {
^Observation.Observable:2:180: note: ‘Observable()’ declared here
@attached(member, names: named(_$observationRegistrar), named(access), named(withMutation)) @attached(memberAttribute) @attached(extension, conformances: Observable) public macro Observable() = #externalMacro(module: “ObservationMacros”, type: “ObservableMacro”)
小伙伴们可以把它们放在 Xcode 的 Command Line Tool 项目中进行测试:
要想发挥 Observable 对象的最大威力,我们需要 SwiftUI 来一拍即合。
在 SwiftUI 中,我们无需再显式调用 withObservationTracking() 方法来监听改变,如虎添翼的 SwiftUI 已为我们自动完成了所有这一切!
struct ContentView: View {
let model = Model(title: "地球超级英雄", heros: [])
var body: some View {
NavigationStack {
Form {
LabeledContent(content: {
Text(model.title)
}, label: {
Text("藏匿点名称")
})
LabeledContent(content: {
Text(model.createAt?.formatted(date: .omitted, time: .standard) ?? "无")
}, label: {
Text("更新时间")
})
Button("刷新") {
// SwiftUI 会自动监听可观察对象的改变,并刷新界面
model.title = "爱丽丝仙境兔子洞"
model.createAt = Date.now
}
}.navigationTitle(model.title)
}
}
}
注意,上面代码中 model 属性只是一个普通的 let 常量,即便如此 model 的改变仍会反映到界面上:
有了 Swift 5.9 中新 Observation 框架加入游戏,在 SwiftUI 5.0 中 EnvironmentObject 再无用武之地,我们仅用 Environment 即可搞定一切!
早在 SwiftUI 1.0 版本时,其就已经提供了 Environment 对应的构造器:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct Environment<Value> : DynamicProperty {...}
有了新 Observation 框架的入驻,结合其 Observable 可观察对象,Environment 可以再次大放异彩:
struct HeroListView: View {
@Environment(Model.self) var model
var body: some View {
List(model.heros) { hero in
HStack {
Text(hero.name)
.font(.headline)
Spacer()
Text("\(hero.power)")
.font(.subheadline)
.foregroundStyle(.gray)
}
}
}
}
struct ContentView: View {
@State var model = Model(title: "地球超级英雄", heros: [
.init(name: "大熊猫侯佩", power: 5),
.init(name: "孙悟空", power: 1000),
.init(name: "哪吒", power: 511)
])
var body: some View {
NavigationStack {
Form {
NavigationLink(destination: HeroListView().environment(model)) {
Text("查看所有英雄")
}
}.navigationTitle(model.title)
}
}
}
现在,即使跨越多重层级关系我们也可以只通过 @Environment 而不用 @EnvironmentObject 来完成状态的间接传递了,是不是很赞呢?
介绍了以上这许多,就还剩一个主题没有涉及:**Observable 对象的可变性!
**
为了能够在子视图中更改对应的可观察对象,我们可以用 @Bindable 修饰传入的 Observable 对象:
struct HeroView: View {
@Bindable var hero: Hero
var body: some View {
Form {
TextField("名称", text: $hero.name)
TextField("力量", text: .init(get: {
String(hero.power)
}, set: {
hero.power = Int($0) ?? 0
}))
}
}
}
不过,对于之前 @Environment 那个例子来说,如何达到子视图能够修改传入的 @Environment 可观察对象呢?
别急,我们可以利用称为“临时可变(Temporary Variable)”的技术将原先不可变的可观察对象改为可变:
extension Hero: Identifiable {
var id: String {
name
}
}
struct HeroListView: View {
@Environment(Model.self) var model
var body: some View {
// 在 body 内将 model 改为可变
@Bindable var model = model
VStack {
List(model.heros) { hero in
HStack {
Text(hero.name)
.font(.headline)
Spacer()
Text("\(hero.power)")
.font(.subheadline)
.foregroundStyle(.gray)
}
}.safeAreaInset(edge: .bottom) {
// 绑定可变 model 中的状态以修改英雄名称
TextField("", text: $model.heros[0].name)
.padding()
}
}
}
}
运行效果如下:
“临时可变”这一技术可以用于视图中任何化“不变”为“可变”的场景中,当然对于直接视图间对象的传递,我们可以使用 @Bindable 这一更为“正统”的方法。
在本篇博文中,我们讨论了在 Swift 5.0 和 SwiftUI 5.0 中大放异彩 Observation 框架的使用,并就诸多技术细节问题给与了详细的介绍,愿君喜欢。
感谢观赏,再会!