Swift 5.9 与 SwiftUI 5.0 中新 Observation 框架应用之深入浅出

0. 概览

Swift 5.9 一声炮响为我们带来全新的宏(Macro)机制,也同时带来了干霄凌云的 Observation 框架。

Observation 框架可以增强通用场景下的使用,也可以搭配 SwiftUI 5.0 而获得双剑合璧的更强威力。

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

  • [0. 概览](#0. 概览)
  • [1. @Observable 宏](#1. @Observable 宏)
  • [2. 通用情境下如何观察 Observable 对象?](#2. 通用情境下如何观察 Observable 对象?)
  • [3. Observable 对象与 SwiftUI 珠联璧合](#3. Observable 对象与 SwiftUI 珠联璧合)
  • [4. 被"抛弃的" @EnvironmentObject](#4. 被“抛弃的” @EnvironmentObject)
  • [5. 在视图中将不可变 Observable 对象转换为可变对象的妙招](#5. 在视图中将不可变 Observable 对象转换为可变对象的妙招)
  • [6. 总结](#6. 总结)

那么,就让我们赶快进入 Observation 奇妙的世界吧!

Let's go!!!😉


1. @Observable 宏

简单来说,Observation 框架为我们提供了集鲁棒性(robust)、安全性、高性能等三大特性为一身的 Swift 全新观察者设计模式。

它的核心功能在于:监视对象状态,并在改变时做出反应!

在 Swift 5.9 中,我们可以非常轻松的通过 @Observable 宏将普通类"转化为"可观察(Observable)类。自然,它们的实例都是可观察的:

swift 复制代码
@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,就是这么简单!

2. 通用情境下如何观察 Observable 对象?

在一个对象成为可观察之后,我们可以通过 withObservationTracking() 方法随时监听它状态的改变:

我们可以将对象需要监听的属性放在 withObservationTracking() 的 apply 闭包中,当且仅当( Hero 中其它属性的改变不予理会)这些属性发生改变时其 onChange 闭包将会被调用:

swift 复制代码
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 点需要注意:

  1. 它默认只会被调用 1 次,所以上面为了能够重复监听,我们在 onChange 闭包里对 watching() 方法再次进行了调用;
  2. withObservationTracking() 方法的 apply 闭包不管如何都会被调用 1 次,即使其监听的属性从未改变过;
  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 项目中进行测试:


3. Observable 对象与 SwiftUI 珠联璧合

要想发挥 Observable 对象的最大威力,我们需要 SwiftUI 来一拍即合。

在 SwiftUI 中,我们无需再显式调用 withObservationTracking() 方法来监听改变,如虎添翼的 SwiftUI 已为我们自动完成了所有这一切!

swift 复制代码
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 的改变仍会反映到界面上:

4. 被"抛弃的" @EnvironmentObject

有了 Swift 5.9 中新 Observation 框架加入游戏,在 SwiftUI 5.0 中 EnvironmentObject 再无用武之地,我们仅用 Environment 即可搞定一切!

早在 SwiftUI 1.0 版本时,其就已经提供了 Environment 对应的构造器:

swift 复制代码
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct Environment<Value> : DynamicProperty {...}

有了新 Observation 框架的入驻,结合其 Observable 可观察对象,Environment 可以再次大放异彩:

swift 复制代码
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 来完成状态的间接传递了,是不是很赞呢?👍🏻

5. 在视图中将不可变 Observable 对象转换为可变对象的妙招

介绍了以上这许多,就还剩一个主题没有涉及:**Observable 对象的可变性!

**

为了能够在子视图中更改对应的可观察对象,我们可以用 @Bindable 修饰传入的 Observable 对象:

swift 复制代码
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)"的技术将原先不可变的可观察对象改为可变:

swift 复制代码
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 这一更为"正统"的方法。

6. 总结

在本篇博文中,我们讨论了在 Swift 5.0 和 SwiftUI 5.0 中大放异彩 Observation 框架的使用,并就诸多技术细节问题给与了详细的介绍,愿君喜欢。

感谢观赏,再会!😎

相关推荐
命运之手1 个月前
【Android】Analysis of Empty Observable in RxJava
android·rxjava·empty·observable
安冬的码畜日常1 个月前
【D3.js in Action 3 精译_031】3.5.2 DIY实战:在 Observable 平台实现带数据标签的 D3 条形图并改造单元测试模块
前端·javascript·单元测试·数据可视化·observable·chai.js
安冬的码畜日常1 个月前
【D3.js in Action 3 精译_028】3.4 小节 DIY 实战:使用 Observable 在线绘制 D3 条形图
前端·javascript·信息可视化·数据可视化·d3.js·observable
大熊猫侯佩4 个月前
SwiftUI 5.0(iOS 17)滚动视图的滚动目标行为(Target Behavior)解惑和实战
滚动·scrollview·swiftui 5.0·ios 17·target behavior·惰性容器·滚动对齐
大熊猫侯佩5 个月前
SwiftUI 5.0(iOS 17)进一步定制 TipKit 外观让撸码如虎添翼
swiftui 5.0·ios 17.0·tipkit·tip·tipviewstyle·自定义 tip 样式·全局调控 tip 外观
大熊猫侯佩7 个月前
SwiftUI 5.0(iOS 17.0)触摸反馈“震荡波”与触发器模式趣谈
触发器·swiftui 5.0·trigger·ios 17.0·haptic·震动反馈·sensoryfeedback