SwiftUI 如何取得 @Environment 中 @Observable 对象的绑定?

概述

从 SwiftUI 5.0(iOS 17)开始,苹果推出了全新的 Observation 框架。它作为下一代内容改变响应者全面参与到数据流和事件流的系统中。

有了 Observation 框架的加持,原本需要多种状态类型的 SwiftUI 视图现在只需要 3 种即可大功告成,它们分别是:@State、@Environment 以及 @Bindable。

在 SwiftUI 中,我们往往会使用 @Environment 来完成视图继承体系中状态的非直接传递,但是在这种情况下我们却无法获取到它的绑定,造成些许不便。

在本篇博文中,我们就来谈谈如何解决这一问题:

  1. ObservableObject 与 @EnvironmentObject 的旧范儿
  2. 问题现象
  3. 解决之道

Let's go!!!;)


1. ObservableObject 与 @EnvironmentObject 的旧范儿

在 SwiftUI 5.0 之前以 @EnvironmentObject 方式跨继承传递状态的视图中,我们可以轻易的获取对应对象的绑定,如下代码所示:

swift 复制代码
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 组合的方式来传递环境变量:

swift 复制代码
@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 语法:

swift 复制代码
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 运行时提出"严正警告":

那么,对此我们还能做些什么呢?

3. 解决之道

一种方法是写一个视图包装器,然后将 Model 对象在其中转换为绑定的形式:

swift 复制代码
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 状态显式转换成了可绑定状态,所以在编译和运行时都没有任何问题。

其实,按照这种思路我们可以再进一步简化实现代码:

swift 复制代码
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 状态。

或者我们干脆一步到位,直接在 body 中使用"原汁原味"的 @Bindable 宏:

swift 复制代码
struct ContentView: View {
    
    @Environment(Model.self) var model
    
    var body: some View {
        @Bindable var bindableModel = model
        
        VStack {
            Text("Main")
            
            Button("Sheeting") {
                bindableModel.isSheeting = true
            }
        }
        .sheet(isPresented: $bindableModel.isSheeting) {
            Text("Sheeting View")
        }
    }
}

现在,我们成功的将 @Environment 中 @Observable 对象的绑定抽取出来,并且彻底摆脱了中间讨厌 wrappedValue 的横插一刀,棒棒哒!💯

希望本文在某些情景下会给小伙伴们一些启迪,若是如此深感欣慰。

总结

在本篇博文中,我们讨论了为什么不能在 SwiftUI 中 @Environment 的 @Observable 对象上使用绑定(Binding),我们随后讨论了如何巧妙地解决这一问题。

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

相关推荐
season_zhu6 小时前
iOS开发:关于日志框架
ios·架构·swift
iOS阿玮9 小时前
苹果2024透明报告看似更加严格的背后是利好!
uni-app·app·apple
大熊猫侯佩10 小时前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩11 小时前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩12 小时前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩12 小时前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple
iOS阿玮1 天前
苹果审核被拒4.1-Copycats过审技巧实操
uni-app·app·apple
大熊猫侯佩1 天前
SwiftUI 6.0(iOS 18)将 Sections 也考虑进自定义容器子视图布局(下)
swiftui·swift·apple