理解SwiftUI数据流(一)

核心设计思想

SwiftUI的View是渲染界面的模型,而不是真正的界面:仅仅包含界面结构、元素和各种属性的描述,并不包含界面像素、绘图缓冲区、绘图上下文等和界面渲染相关的内容。

View可以理解接受界面Data作为参数,输出View的函数:View = func(Data)

struct ContactUs: View { 
    var body: some View { // ... } 
}
复制代码

View在SwiftUI都是值类型

  • 所有组件都无法通过间接引用的方式进行修改。
  • 所有影响到View的依赖关系都必须通过创建时注入到View对象里。
struct ContactUs: View { 
    init(contactUsViewModel: ContactUsViewModel)  { 
        self.viewModel = contactUsViewModel
    } 
}
复制代码

既然View是Struct它又如何动态的描述一个View的呢?

View会产生用户交互、会产生新的数据、会引起View的变化,而Struct是一个静态的值类型,他是如何做到动态变化的呢?

SwiftUI中的Property wrapper

@State

struct ContentView { 
    @State var content: String 
    var body: some View {
    TextField("Placeholder", $content) 
    } 
}
复制代码

@State 是 ContentView 的一个可信数据源。只要被 @State 修饰,通过它就可以控制 ContentView 上显示的内容了。

@State 是一个 property wrapper,在定义 TextField 的时候,使用了 $content 的形式。因此,我们不妨直接到 State 的定义中,看看它的两个最重要的属性wrappedValue 和 projectedValue 究竟是什么。

struct State<Value> { 
    var projectedValue: Binding<Value> 
    var wrappedValue: Value 
}
复制代码

Value 的类型是 StringState 的类型是 State<String>wrappedValue的类型是 ValueprojectedValue 的类型是 Binding<String>

TextField 的 init 方法:

init<S>( 
    _ title: S,
    text: Binding<String>, 
    onEditingChanged: @escaping (Bool) -> Void = { _ in }, 
    onCommit: @escaping () -> Void = {}) where S : StringProtocol
复制代码

它的 text 参数也接受一个 Binding<String> 对象,这也就是为什么我们可以用 TextField("Placeholder", $content) 的形式创建 TextField 了。那么,这个 Binding 又是什么呢?

@Binding

在 Apple 的文档中,对 Binding 的描述是这样的:

扫描二维码关注公众号,回复: 14225107 查看本文章

Use a binding to create a two-way connection between a view and its underlying model.

$content 在 ContentView 代表的界面和 content 代表的数据之间,创建了双向绑定。

这是怎么实现的呢?

init(get: () -> Value, set: (Value) -> Void)
复制代码

Binding 本质就是一个 getter 和 setter,那么getter 究竟是从哪读数据?setter 又设置了什么呢?我们结合下面这张图解释这个问题:

1653277355896.png

ContentView.content 作为一个 property wrapper,它的 projectValue 是一个 Binding<String> 对象,这个 Binding<String> 的 getter 和 setter 对应着 wrappedValue 的 get 和 set 方法。

当用户在 ContentView 内置的 TextField 中输入内容时候,TextField 就可以通过注入的 Binding 对象间接修改 content 属性的值了。

由于 content 又是一个 @State 对象,SwiftUI 运行时就会在它的渲染队列中加入所有依赖这个属性的界面的渲染任务。当屏幕刷新的时候,所有受影响的部分就更新过来了。言外之意,属性变化到界面变化的过程,是一个串行的过程,只不过这个过程发生的很快(大部分 iOS 设备上都是每秒 60 次,iPad pro 则可以达到每秒 120 次),它并不会让我们感觉到有任何延迟。

总结

@State 非常适合 struct 或者 enum 这样的值类型,它可以自动为我们完成从状态 到 UI 更新等一系列操作。但是它本身也有一些限制,我们在使用 @State 之前,对 于需要传递的状态,最好关心和审视下面这两个问题:

  1. 这个状态是属于单个 View 及其子层级,还是需要在平行的部件之间传递和使用?@State 可以依靠 SwiftUI 框架完成 View 的自动订阅和刷新,但这是有条件的:对于 @State 修饰的属性的访问,只能发生在 body 或者 body 所调 用的方法中。你不能在外部改变 @State 的值,它的所有相关操作和状态改变都应该是和当前 View 挂钩的。如果你需要在多个 View 中共享数据,@State 可能不是很好的选择;如果还需要在 View 外部操作数据,那么 @State 甚至就不是可选项了。

  2. 状态对应的数据结构是否足够简单?对于像是单个的 Bool 或者 String, @State 可以迅速对应。含有少数几个成员变量的值类型,也许使用 @State 也还不错。但是对于更复杂的情况,例如含有很多属性和方法的类型,可能其中只有很少几个属性需要触发 UI 更新,也可能各个属性之间彼此有关联,那么我们应该选择引用类型和更灵活的可自定义方式。

对于这样的不适合选择 @State 的情况 (往往这是实际数据传递中 更普遍的情况),ObservableObject@ObservedObject 是解决的方案。

思考

@State 属性值和普通属性在View模型表现。

猜你喜欢

转载自juejin.im/post/7100774897281925128