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-)

相关推荐
Daniel_Coder14 小时前
Xcode 中常用图片格式详解
ios·xcode·swift
帅次14 小时前
Objective-C面向对象编程:类、对象、方法详解(保姆级教程)
flutter·macos·ios·objective-c·iphone·swift·safari
RyanGo17 小时前
iOS断点下载
ios·swift
杂雾无尘20 小时前
掌握生死时速:苹果应用加急审核全攻略!
ios·swift·apple
HarderCoder20 小时前
Swift 6.2 中的 `@concurrent`
ios·swift
YungFan1 天前
iOS26适配指南之通知
ios·swift
大熊猫侯佩11 天前
消失的它:摆脱 SwiftUI 中“嵌入视图数量不能超过 10 个”限制的秘密
swiftui·swift·apple
大熊猫侯佩11 天前
Swift 抛砖引玉:从数组访问越界想到的“可抛出错误”属性
swift·apple
大熊猫侯佩11 天前
ruby、Python 以及 Swift 语言关于 “Finally” 实现的趣谈
python·ruby·swift
minos.cpp11 天前
从厨房到代码台:用做菜思维理解iOS开发 - Swift入门篇①
ios·蓝桥杯·swift