摘自《SwiftUI和Combine编程》---《数据状态和绑定》
总结:
-
根据适用范围和存储状态的复杂度的不同,需要选取合适的方案。
-
@State 和 @Binding 提供 View 内部的状态存储,它们应该是被标记为 private 的简单值类型,仅在内部使用。
-
ObservableObject 和 @ObservedObject 则针对跨越 View 层级的状态共享,它可以处理更复杂的数据类型,其引用类型的特点,也让我们需要在数据变化时通过某种手段向外发送通知 (比如手动调用 objectWillChange.send() 或者使用 @Published),来触发界面刷新。
-
对于“跳跃式”跨越多个 View 层级的状态,@EnvironmentObject 能让我们更方便地使用 ObservableObject,以达到简化代码的目的。
-
如果你纠结于选择使用哪种方式的话,从 ObservableObject 开始入手会是一个相对好的选择:如果发现状态可以被限制在同一个 View 层级中,则改用 @State;如果发现状态需要大批量共享,则改用 @EnvironmentObject。
State
@State 数据状态驱动界面
限制一:对于 @State 修饰的属性的访问,只能发生在 body 或者 body 所调用的方法中。不能在外部改变 @State 的值,它的所有相关操作和状态改变都应该是和当前 View 挂钩的。
限制二:对于复杂的情况,例如含有很多属性和方法的类型,可能其中只有很少几个属性需要触发 UI 更新,也可能各个属性之间彼此有关联,那么我们应该选择引用类型和更灵活的可自定义方式。
和一般的存储属性不同,@State 修饰的值,在 SwiftUI 内部会被自动转换为一对 setter 和 getter,对这个属性进行赋值的操作将会触发 View 的刷新,它的 body 会被再次调用,底层渲染引擎会找出界面上被改变的部分,根据新的属性值计算出新的 View,并进行刷新。
@State 属性值仅只能在属性本身被设置时会触发 UI 刷新,这个特性让它非常适合用来声明 struct 或者 enum 这样的值类型:因为对值类型的属性的变更,也会触发整个值的重新设置,进而刷新 UI。不过,在把这样的值在不同对象间传递时,状态值将会遵守值语义发生复制。
因此需要配合 binding 使用进行引用传递。
Binding
@Binding 也是对属性的修饰,它做的事情是将值语义的属性“转换”为引用语义。
对被声明为 @Binding 的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递。
在传递时,我们在它前面加上美元符号 $。在 Swift 5.1 中,对一个由 @ 符号修饰的属性,在它前面使用 $ 所取得的值,被称为投影属性 (projection property)。有些 @ 属性,比如这里的 @State 和 @Binding,它们的投影属性就是自身所对应值的 Binding 类型。不过要注意的是,并不是所有的 @ 属性都提供 $ 的投影访问方式。
例如:$brain 的写法将 brain 从 State 转换成了引用语义的 Binding,并向下传递。
propertyWrapper
属性包装特性给了我们一个机会,可以在一定程度上简化语言的模板代码,并且通过“标注”的方式来改变特性。
它与自定义 getter 和 setter 做的事情相似,只不过功能更强大,且不需要到处重复去写一样的代码。
在 Swift 中,这一特性的正式名称是属性包装 (Property Wrapper)。不论是 @State,@Binding,@ObjectBinding @EnvironmentObject,它们都是被 @propertyWrapper 修饰的 struct 类型。
SwiftUI 中 State 定义的关键部分如下:
@propertyWrapper
public struct State<Value> :
DynamicViewProperty, BindingConvertible
{
// init(initialValue:),wrappedValue 和 projectedValue 构成了一个 propertyWrapper 最重要的部分。
public init(initialValue value: Value)
public var value: Value { get nonmutating set }
public var wrappedValue: Value { get nonmutating set }
public var projectedValue: Binding<Value> { get }
}
-
initialValue 这个参数名相对特殊:当它出现在 init 方法的第一个参数位置时,编译器将允许我们在声明的时候直接为 @State var brain 进行赋值。
-
对 包装属性 进行赋值,看起来也就和普通的变量赋值没有区别。但是,实际上这些调用都触发的是属性包装中的 wrappedValue。
-
使用$符号前缀访问,其实访问的是 projectedValue 属性。在 State 中,这个属性返回一个 Binding 类型的值,通过遵守 BindingConvertible,State 暴露了修改其内部存储的方法,这也就是为什么传递 Binding 可以让属性具有引用语义的原因。
ObservableObject
如果说 @State 是全自动驾驶的话,ObservableObject 就是半自动,它需要一些额外的声明。
ObservableObject 协议要求实现类型是 class,它只有一个需要实现的属性:objectWillChange。在数据将要发生改变时,这个属性用来向外进行“广播”,它的订阅者 (一般是 View 相关的逻辑) 在收到通知后,对 View 进行刷新。
一般情况下,我们使用一个 PassthroughSubject 实例作为 objectWillChange 的值。PassthroughSubject 提供了一个 send 方法,来通知外界有事件要发生了 (此处的事件即驱动 UI 的数据将要发生改变)。
例如在 CalculatorModel 里添加 CalculatorBrain:
class CalculatorModel: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var brain: CalculatorBrain = .left("0") {
willSet { objectWillChange.send() }
}
}
外部使用:
Text(model.brain.output) // 2
//...
CalculatorButtonPad(brain: $model.brain)
model 的 $ 投影属性返回的是一个 Binding 的内部 Wrapper 类型,对它再进行属性访问,将会通过动态查找的方式获取到对应的 Binding<CalculatorBrain>。
Published
在 ObservableObject 中,如果没有定义 objectWillChange,编译器会为你自动生成它,并在被标记为 @Published 的属性发生变更时,自动去调用 objectWillChange.send()。这样就省去了我们一个个添加 willSet 的麻烦。
class CalculatorModel: ObservableObject {
@Published var brain: CalculatorBrain = .left("0")
}
EnvironmentObject
在 SwiftUI 中,View 提供了 environmentObject(_:)
方法,来把某个 ObservableObject 的值
注入到当前 View 层级及其子层级中去。在这个 View 的子层级中,可以使用 @EnvironmentObject 来直接获取这个绑定的环境值。
在对应的 View 生成时,我们不需要手动为被标记为 @EnvironmentObject 的值进行指定,它们会自动去查询 View 的 Environment 中是否有符合的类型的值,如果有则使用它们,如没有则 抛出运行时
的错误。
需要在创建 View 的地方,通过 environmentObject 把通用的 Model 添加上去,为了让预览也能保持工作,需要在 View_Previews 中用同样的方式添加上 environmentObject
window.rootViewController = UIHostingController( rootView:
ContentView().environmentObject(CalculatorModel())
)
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(CalculatorModel())
}
}
网友评论