837994dd82f6c2f2b360fe3690a8812f
WWDC20 10040 - SwiftUI 编程指南

前言


时光荏苒,SwiftUI 技术已经推出一年,从 WWDC 2020 来看,SwiftUI 团队付出了空前的努力,使得 SwiftUI 无论是在开发体验,还是性能上都得到了很大的提升。如果说 SwiftUI 是去年苹果在开发技术转型上的小试牛刀,那么今年的 SwiftUI 基本已经成为了未来 5-10 年苹果生态开发技术的主流方式。

众所周知, SwiftUI 是一种数据驱动型的 DSL。也就是说,所有的界面显示效果,必须去改变数据然后通过绑定才能体现到相应的视图上。那么接下来,设计和组织好这些数据结构,才能让 SwiftUI 发挥出它的特性和优势。

回顾


WWDC 2019 Data Flow Through SwiftUI Session 对应的文章 SwiftUI 数据流 有幸也是笔者撰写,文章介绍了SwiftUI 数据流的基本原理、绘制流程以及发展趋势,并且对比了各种不同的前端编程范式,感兴趣的读者可以回顾阅读。

大纲


去年 SwiftUI 给出了 @State,@Binding,@ObservedObject,@EnvironmentObject 几个 PropertyWrapper 来处理视图和数据之间的绑定依赖关系,但并没有详细的讲解这几个 PropertyWrapper 的使用场景以及区别,后续大家都是根据自己的编码和调试经验得出一些使用上的技巧,今年苹果直接拿出一个 Book Club App 做为案例,全方位的给你讲解如何使用它们,并给出一些参考规范,真香!本 Session 是由 Curt CliftonLuca、Raj Ramamurthy 三位大神为我们讲解,议题主要围绕以下三个方面:

  • 视图和数据的生命周期
  • @StateObject 和一些新特性
  • 值类型和引用类型数据在 SwiftUI 中的处理

三步走编写 SwiftUI


从本质上讲,编写 SwiftUI 应用程序还是属于前端技术的范畴,所以写界面是每位开发者都应该掌握的技能,那么当大家拿到设计稿开始编码的时候,首先要思考以下三个问题:

  • 如何建立视图元素与数据的对应关系
  • 视图和数据之间的操作逻辑有哪些
  • 数据由谁持有

Property(属性绑定)

那么现在我们用上面这个设计图来回答以上三个问题:

  • 这个界面是个读书的列表,每条书籍信息里有书籍的封面、数据的名称作者以及读书的进度
  • 这个列表只是用来展示书籍列表,没有对数据的更改操作
  • 这个图书数据是由每个 BookCard 持有,实例化的时候从父视图传递进来

通过回答上面问题,很容易就可以编写上述代码,可以看出 BookCard 视图的数据是由 Book 这个结构体提供的,Book 数据结构中包含了书籍的封面、标题、作者等信息,Progress 用来标记读书的进度;这个书籍列表视图只是用来展示数据,所以 book 和 progress 都用 let 声明为常量。那这些数据从哪来?他们可以通过在实例化 BookCard 的时候,通过构造函数从父视图传进来,每次渲染调用 body 计算属性获取绘制信息时,BookCard 都会实例一次。这个新实例化的 BookCard 生命周期只局限于本次渲染,如果下次渲染流程里它不再需要展示,那么它就被销毁了。它的可视化视图层级以及对应的数据关系图,如下:

接下来我们进入书籍详情页面,设计图如下,当点击 Update Progress 按钮的时候,需要弹出一个弹层,来记录读书进度。

根据信息展示的对应关系,我们需要一个 Boolean 变量 isEditorPresented 来控制弹层的显示与否,需要一个 String 类型的 note 来记录备注信息,需要一个 Double 类型的 progress 来记录读书进度,代码如下:

通常情况下可以把这些数据组装到一个 EditorConfig 的结构体中,如下图:

这样组装成 EditorConfig 后,BookCard 代码一下就清晰了,也非常方便进行独立测试。由于 EditorConfig 是一个值类型,改变它的内部属性它本身也会发生改变。

@State(状态绑定)

以上我们处理完弹层视图与数据之间的信息展示对应关系,那么接下来处理第二个问题 - “视图和数据之间的操作逻辑有哪些“,当我们要展示弹层的时候,我们需要将 isEditorPresented 属性设置为 true,那么就需要给 EditorConfig 添加一个 mutating 方法来进行更改并且初始化一些其他信息,如下图:

最后来处理第三个问题 - ”数据由谁持有“,从代码看 EditorConfig 上的数据都是 BookView 视图本地持有,没有从父视图传递进来,再结合上面数据需要被更改的情况,这时候需要创建一个单一数据源以维持数据与视图之间的同步。在 SwiftUI 中创建单一数据源最简单的方法就是 @State Property Wrapper,用 @State 来修饰属性后,SwiftUI 便接管了被修饰属性的存储 (简单理解就是 Get, Set 方法)。那么 SwiftUI 为什么要这么处理呢?因为如果是一个单纯的结构体,每次在父视图调用 body 的时候,都是实例化一个全新 EditorConfig 结构体,对于只是展示数据的视图是没问题的,但是如果视图中有修改数据的行为,那么被修改的数据在结构体被销毁的时候也丢失了,而 SwiftUI 通过 @State PropertyWrapper,帮我们在内存里维持住 EditorConfig,每次绘制的时候从内存缓存的数据中再读出来。

@Binding(共享绑定)

那接下来让我们再用三步走法则,思考下 ProgressEditor (弹层视图) 该怎么实现,在这里值得注意的是弹层的数据从哪来的,由谁持有?如果我们先假设 ProgressEditor (弹层视图) 自己持有 EditorConfig,直接在它内部声明为一个属性,然后在实例化的时候从 BookView 传递进来,这样做只是传递了一份数据拷贝,当 ProgressEditor (弹层视图) 要修改 EditorConfig 的值时,也只是对自己内部的拷贝进行更改,不会影响到 BookView 中的 EditorConfig 实例,所以两边的数据是不同步的。那么我们给 ProgressEditor (弹层视图) 的 EditorConfig 也添加上一个 @State PropertyWrapper 可以吗?答案是否定的,这样做相当于在 BookView 和 ProgressEditor (弹层视图) 里创建了两个数据源,但是我们需要一个数据源来保持两个视图的数据同步,在 SwiftUI 中这种共享数据源的方式可以用 @Binding PropertyWrapper 来实现。使用 @Binding 后,现在 SwiftUI 帮你接管 EditorConfig 而且 BookView 和 ProgressEditor 都依赖于这个共享数据源。所以当数据发生变化时,SwiftUI 会对两个视图进行重绘。

接下来 ProgressEditor 还需要把修改好的数据传递给 BookView 进行展示,SwiftUI 采用了 PropertyWrapper 的 Projection 特性来实现了双向数据绑定,所以只需要在参数前面加一个 $ 符号即可。最终相当于 ProgressEditor (弹层视图) 直接跟 BookView 里的 EditorConfig 数据建立依赖关系。很多 SwiftUI 默认控件也采用的了这种绑定声明机制。

注意:在 State 前加 $ 的方式实现来声明 Binding 的依赖关系,只是其中一种写法, 其实 ObservableObject 和 Binding 也都是可以通过 $ 获取到 Binding 类型的 projectedValue 的。

三步走法则

  • 如何建立视图元素与数据的对应关系
  • 视图和数据之间的操作逻辑有哪些
  • 数据由谁持有

小技巧

  • 当视图上只是对数据的展示时,用 Property (一般属性)
  • 当视图要修改数据,而且只是在当前视图中短暂用到时,用 @State (状态绑定)
  • @State 或者 @Published 修饰的数据要被传递到其他视图访问且需要修改时,用 @Binding(共享绑定)

设计好数据中间层


使用 ObservableObject

@State 主要是针对视图内短暂的状态处理,但是通常情况下,UI 代码和业务逻辑代码是分开的。这个时候想要把业务逻辑代码绑定到 SwiftUI 视图中,就需要用到 ObservableObject。首先我们来看下ObservableObject 协议是如何定义的:

  • 它遵循 AnyObject 协议,所以只能是引用类型遵循来实现它
  • 需要实现 objectWillChange 属性,这个属性是个 Publisher,通过它来告诉 SwiftUI 数据将要发生变化,然后触发重绘
  • SwiftUI 提供了默认的 Publisher 来处理 @Published 修饰的属性,当然也可以通过自定义 Publisher 来处理数据变化

ObservableObject 中间层

我们可以把 ObservableObject 理解为视图和数据建立依赖关系的中间层(类似 ViewModel),ObservableObject 内部不仅仅包含要展示到视图上的数据,也可以处理业务逻辑,数据缓存,网络请求等操作。当然你可以根据自己的业务逻辑来定义 ObservableObject 的生命周期。比如可以将所有的数据都包含在一个 ObservableObject 中,所有的视图都通过这个 ObservableObject 与相对应的数据建立依赖关系,如下图:

当你的数据结构非常复杂的时候,也可以拆分多个 ObservableObject 分别管理对应的视图数据,处理起来非常灵活,如下图:

@Published 属性修饰器

top Created with Sketch.