在 SwiftUI 中传递 @Observable 对象的三种方式
所属系列
- Swift 与 SwiftUI 系列 (3 共 5)
环境注入、直接初始化和 @Binding,是在 SwiftUI 中向子视图传递 @Observable 对象或其一部分的三种不同方式。表面上它们很像,但在所有权、耦合程度和接口范围上的语义有明显区别。
1. .environment / @Environment
将对象注入视图树,让任何后代视图按类型取出它。
// Parent
ContentView()
.environment(state)
// Child (anywhere in the subtree)
@Environment(AppState.self) var state
对象会隐式地沿整个视图层级传递。任何后代都可以选择使用它,不必经过中间视图层层转交。
- 子视图拿到同一个引用,修改在各处都可见
- 没有初始化器参数,调用处看不见这个依赖
- 忘记注入时,会在运行时崩溃,而不是编译时报错
最适合整个应用或子树共享的状态:认证会话、导航模型、用户偏好、主题。
2. 直接初始化:Subview(state: state)
通过子视图的初始化器传入对象,将其保存为普通存储属性。
// Child
struct Subview: View {
let state: AppState
var body: some View {
Text(state.title)
}
}
// Parent
Subview(state: state)
由于 @Observable 对象是类,子视图持有的是同一实例的引用。子视图做出的修改会自动反映到父视图,不需要 @Binding。
- 依赖明确,在调用处就能看见
- 编译期安全,由类型系统强制保证
- 最容易理解,也最容易单独做单元测试
最适合紧密关联的父子视图:多写一个参数,换来一目了然的关系,是值得的。
3. @Binding
传递针对单个属性投影出的绑定,而不是整个对象。
// Parent: @Bindable lets you project $ bindings from @Observable
@Bindable var state: AppState
NameField(name: $state.username)
// Child
struct NameField: View {
@Binding var name: String
var body: some View {
TextField("Username", text: $name)
}
}
子视图获得的是与某个具体值的双向连接,不需要知道父级模型的类型。写入会自动传回数据源。
- 接口范围最小,子视图只看到自己需要的内容
- 组件可以在不同模型类型之间复用
- 父视图一侧需要
@Bindable,才能从@Observable对象生成$投影 - 对于值类型,这也是合适的模式,因为
@Observable仅支持类
最适合不应依赖特定模型类型的可复用或通用子视图,也适合基本值类型。
速查
| 模式 | 语法 | 传递内容 | 耦合情况 |
|---|---|---|---|
.environment |
@Environment(T.self) |
整个对象,隐式传递 | 任意深度,缺失时在运行时崩溃 |
| 直接初始化 | let state: T |
整个对象,显式传递 | 编译期安全,与父级模型关联更紧密 |
@Binding |
@Binding var x: T |
单个属性,双向连接 | 可复用,需要父级使用 @Bindable |
核心区别在于:环境注入和直接初始化都会以引用语义传递整个对象,并允许完整的修改操作。@Binding 传递的是针对某个属性投影出的绑定:读写范围有限,也不包含对象引用。想做可复用的组件时,选择 @Binding;可以接受与特定模型耦合时,就选择另外两种。