概述
从 SwiftUI 5.0(iOS 17)开始,苹果推出了全新的 Observation 框架。它作为下一代内容改变响应者全面参与到数据流和事件流的系统中。
有了 Observation 框架的加持,原本需要多种状态类型的 SwiftUI 视图现在只需要 3 种即可大功告成,它们分别是:@State、@Environment 以及 @Bindable。
在 SwiftUI 中,我们往往会使用 @Environment 来完成视图继承体系中状态的非直接传递,但是在这种情况下我们却无法获取到它的绑定,造成些许不便。
在本篇博文中,我们就来谈谈如何解决这一问题:
- 概述
- 1. ObservableObject 与 @EnvironmentObject 的旧范儿
- 2. 问题现象
- 3. 解决之道
- 总结
Let‘s go!!!😉
本文对应的视频课在此,欢迎小伙伴们恣意观赏!
SwiftUI 如何取得 @Environment 中的绑定
1. ObservableObject 与 @EnvironmentObject 的旧范儿
在 SwiftUI 5.0 之前以 @EnvironmentObject 方式跨继承传递状态的视图中,我们可以轻易的获取对应对象的绑定,如下代码所示:
class OldModel: ObservableObject {
@Published var isSheeting = false
}
struct Home: View {
@EnvironmentObject var oldModel: OldModel
var body: some View {
Text("Home")
.sheet(isPresented: $oldModel.isSheeting) {
Text("Good to go!!!")
}
}
}
从上面代码可以看到,用 @EnvironmentObject 修饰的模型类型 oldModel 会“自动”产生对应的绑定形态 $oldModel,这样我们就可以很方便的将其绑定传递到需要的视图中去。
那么,用 Observation 框架中新的 @Observable 和 @Environment 组合来传递跨视图继承体系的状态又会如何呢?
让我们一窥究竟。
2. 问题现象
现在将上面的代码修改为 @Observable 和 @Environment 组合的方式来传递环境变量:
@Observable
class Model {
var isSheeting = false
}
struct ContentView: View {
@Environment(Model.self) var model
var body: some View {
Text("Main")
.sheet(isPresented: $model.isSheeting) {
Text("Sheeting View")
}
}
}
那么问题来了,此时编译器会大声抱怨:根本没有 $model 这样的东西存在!
可见使用 @Environment(Model.self) 定义的状态对象没有自动生成对应的值绑定,即使 Model 绝对是可观察的(意味着背后一定潜伏着绑定“幽灵”)。
诚然,一种看似简单的解决方法就是使用 Swift 5.9 中新的内嵌 @State 语法:
struct ContentView: View {
@Environment(Model.self) var model
var body: some View {
@State var stateModel = model
VStack {
Text("Main")
Button("Sheeting") {
stateModel.isSheeting = true
}
}
.sheet(isPresented: $stateModel.isSheeting) {
Text("Sheeting View")
}
}
}
不过这种方式会导致 @State 状态处在创建视图的“外部”,可能会将其变为常量从而阻止实际值的更新。当然,这只是一种潜在的可能性,也可能我们 App 运行的毫无问题。不过,无论如何调试器都会在 App 运行时提出“严正警告”:
那么,对此我们还能做些什么呢?
更多关于 Observation 框架以及 @Observable 宏的介绍,请小伙伴们移步如下链接观赏精彩的博文:
- Swift 5.9 与 SwiftUI 5.0 中新 Observation 框架应用之深入浅出
- Swift 5.9 新 @Observable 对象在 SwiftUI 使用中的陷阱与解决
3. 解决之道
一种方法是写一个视图包装器,然后将 Model 对象在其中转换为绑定的形式:
struct ContentView: View {
@Environment(Model.self) var model
@ViewBuilder
func createBody() -> some View {
let bindableModel = Bindable(model)
VStack {
Text("Main")
Button("Sheeting") {
bindableModel.wrappedValue.isSheeting = true
}
}
.sheet(isPresented: bindableModel.isSheeting) {
Text("Sheeting View")
}
}
var body: some View {
createBody()
}
}
因为我们将原来的 @Environment 状态显式转换成了可绑定状态,所以在编译和运行时都没有任何问题。
其实,按照这种思路我们可以再进一步简化实现代码:
struct ContentView: View {
@Environment(Model.self) var model
var body: some View {
let bindableModel = Bindable(model)
VStack {
Text("Main")
Button("Sheeting") {
bindableModel.wrappedValue.isSheeting = true
}
}
.sheet(isPresented: bindableModel.isSheeting) {
Text("Sheeting View")
}
}
}
如上代码所示,我们还是使用内联变量定义。不过所不同的是,这次我们创建的是 model 对应的可绑定值而不是状态值。所以这次运行不会有任何问题,因为我们没有在外部创建“孤苦伶仃”的 @State 状态。
希望本文在某些情景下会给小伙伴们一些启迪,若是如此深感欣慰。
想要进一步系统学习 Swift 的小伙伴们,欢迎来我的《Swift语言开发精讲》专栏逛一逛哦:
- Swift 语言开发精讲
总结
在本篇博文中,我们讨论了为什么不能在 SwiftUI 中 @Environment 的 @Observable 对象上使用绑定(Binding),我们随后讨论了如何巧妙地解决这一问题。
感谢观赏,再会啦!😎