美文网首页从0到1学习SwiftUI
SwiftUI 数据状态和绑定

SwiftUI 数据状态和绑定

作者: 一粒咸瓜子 | 来源:发表于2021-12-09 13:58 被阅读0次

    摘自《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())
        }
    }
    

    相关文章

      网友评论

        本文标题:SwiftUI 数据状态和绑定

        本文链接:https://www.haomeiwen.com/subject/rqwoxrtx.html