[Demystify SwiftUI](揭开 SwiftUI 的奥秘面纱)内容根据 《WWDC21: 10022-Session》

WWDC21 | Demystify SwiftUI

一、常识回忆

SwiftUI 从**《WWDC19》**发布到现在,咱们或多或少都接触过了。在讲 Demystify SwiftUI 之前,咱们先来简单回忆一下 SwiftUI :

什么是 SwiftUI?

“SwiftUI is a declarative UI framework” — Apple

“SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift.” — Apple

咱们最初认识 SwiftUI 这个词的时分,第一正常反响便是会问,“什么是 SwiftUI?“ ,而 Apple 官方给出的解释是:

  • SwiftUI 是一个声明式的 UI 结构。它根据 Swift,经过一种立异且特别简单的办法去构建用户界面,支持跨一切的 Apple 渠道。

单纯看这个简略的描绘,咱们或许并不能在脑海中把它具象化。咱们需求举一个比如,来对这个解释进行补充。声明式简单的来说便是描绘式,咱们要完成左下图的界面,描绘式可所以这样的:

一个界面里有一个笔直的布局(VStack),笔直布局里边有一个开关(Taggle),开关状况和 isOn 绑定,(isOn 便是记录开关状况的);然后布局里边还有一个文本描绘(Text),本文内容也和 isOn 进行相关,经过 isOn 的状况来显示开或关。

上面描绘的每一句咱们都能很直观的从下面代码中找到对应的代码块。咱们写的代码有层次结构且越趋同于描绘,越趋同于实际表达,这便是声明式。

WWDC21 | Demystify SwiftUI

SwiftUI 根据 Swift,Swift 的语法现已很简单快捷了,可是 SwiftUI 再此基础上又进行深度封装,语法更为简练。以及结合强壮的 Xcode,构建界面就好像搭积木相同简单,并且代码能与预览界面实时同步,十分的简单且赋有立异。

当然 SwiftUI 冷艳之处还有一个便是能够跨一切 Apple 渠道(iOS、ipadOS、tvOS、macOS、watchOS),只需求一套代码就能多端运转。

二、揭开 SwiftUI 的奥秘面纱

到这儿咱们对 SwiftUI 有了开始的认识,从上面小节咱们知道,SwiftUI 是一个声明式 UI 结构,咱们在最上层经过 SwiftUI 去描绘一个 App,体验着 SwiftUI 给咱们带来的快捷,但 SwiftUI 在暗地所做的事情,咱们还不甚了了。所以咱们今日就来揭开 SwiftUI 的奥秘面纱,从暗地窥视 SwiftUI 的三大核心要素:

  • Identity (身份标识)– 在程序多次更新中,辨认相同或不同视图的办法
  • Lifetime(生命周期)– SwiftUI 随时追踪视图和数据存在的办法
  • Dependencies (依靠项)– 使 SwiftUI 理解界面何时更新以及为何更新

咱们就逐一讨论一下这些概念。

Identity (身份标识)

下图里有两张可爱的小狗图片,咱们经过什么办法能区别它们是同一只小狗相片,还是两只不同的小狗相片?

WWDC21 | Demystify SwiftUI

事实上咱们并不能经过这两张图片直接区别出来他们是不是同一只小狗。

WWDC21 | Demystify SwiftUI

那假如咱们在图片下边标识出小狗姓名,他们用的是同一个姓名,咱们大致能猜出是同一只小狗。当然再严谨点便是给小狗办身份证。

WWDC21 | Demystify SwiftUI

那假如图片下面标识出小狗的姓名不是同一个,那咱们能必定两张图片上面的小狗不是同一只。

这个便是身份标识的好处,SwiftUI 辨认视图办法也是相同,但 SwiftUI 运用的身份标识有两种类型:

Explicit Identity(显式身份)

上面咱们举的比如,给小狗图片分配名称或者说是标识符,这是显式身份的一种形式。而咱们在 UIKit 和 AppKit 常用的显式身份便是指针身份,下面是 UIKit 或 AppKit 的视图层级结构,图上的 UIView 和 NSView ,它们每个都有一个指向它们的内存分配的仅有指针,这个指针便是指针身份,也是显式身份的一种形式。咱们能够只运用它们的指针,来引证单个视图。假如两个视图共享同一个指针咱们确认这两是视图同一个视图。

WWDC21 | Demystify SwiftUI

可是 SwiftUI 不运用指针,由于 SwiftUI 视图是值类型。为什么运用值类型?一个是值类型相对而言更高效且节省功能,一个是能够使代码更干净且更好的阻隔状况。对这块感兴趣的同学能够看一下 《WWDC19: SwiftUI Essentials》这个 session。

WWDC21 | Demystify SwiftUI

尽管 SwiftUI 不运用指针身份,但 SwiftUI 依靠于其他形式的显式身份。比如说 ForEach id 参数是显式标识的一种形式。咱们能够经过自定义 id 明晰对应的视图。

ForEach(..., id: \.someProperty) { ... }

咱们再看一个比如,下面是一个运用了 ScrollViewReader 的视图,在底部有一个按钮。头部文本绑定咱们自定义的标识符,点击按钮,按钮直接回到顶部。从代码很直观的看到,咱们将该标识符传递给翻滚视图代理的 scrollTo 办法,告诉 SwiftUI ,假如点击了按钮,就翻滚到该指定视图。

WWDC21 | Demystify SwiftUI

咱们并不是都需求明晰每个视图的 id,比如说 ScrollViewReaderScrollViewButton 等视图是不需求自定义 id 的,咱们只需求给被其他地方引证的视图添加上 id

当然不需求显式身份并不意味着没有身份标识,每个视图都有一个身份标识,即使不是显式身份,也都是有的。这时分引入 Struct Identity (结构身份)的概念。

Struct Identity(结构身份)

SwiftUI 运用的视图层次结构,能自动为视图生成隐式身份,也便是 Struct Identity(结构身份)。咱们举小狗的比如,下面是别的两张小狗的图片,假定咱们无法知道他们的姓名,这时分咱们应该怎么去区别他们呢?咱们能够经过他们坐的方位来辨认他们,比如“左边的狗” 和 “右边的狗”。咱们对这种相对排列区别它们的办法,叫做结构身份。

SwiftUI 在整个 API 中都是利用了结构身份。举个常见的比如,咱们运用 if...else... 条件句子时分,咱们是能明晰的辨认每个视图。如下面代码,第一个视图仅在条件为真时显示,而第二个视图仅在条件为假时显示。是不是跟上面小狗的比如很类似,上个比如是经过左右来标识小狗,而这次是经过真假的办法来确认视图。

WWDC21 | Demystify SwiftUI

上面的写的是 if...else... ,但 SwiftUI 内部看到的是确是右下图的姿态。编译器会把 if 句子转译为 _ConditionalContent 视图。这种转译是经过 ViewBuilder 完成的,它是 Swift 中的一种结果构建器。View 协议默认将它的 body 特点包装在一个 ViewBuilder中。

WWDC21 | Demystify SwiftUI

代码中 body 特点的 View 回来类型是一个占位符,代表这是一个静态复合类型。运用这种泛型的类型, SwiftUI 能够明晰区别两个视图。SwiftUI 也在暗地为它们各分配一个隐式身份。

WWDC21 | Demystify SwiftUI
WWDC21 | Demystify SwiftUI

这儿官方也给出了一个建议,便是假如 if...else... 里是同一个 View,可是参数条件不同,咱们直接运用三目运算符来替代。尽管两种做法都能够,但运用三目运算符能够让这两个视图坚持同一个身份,这样也能提供更流通的过渡,也有助于坚持视图的生命周期和状况。

// 官方不引荐写法
if isGood {
    PawView(tint: .green).frame(maxHeight: .infinity, alignment: .top)
    Spacer()
} else {
    Spacer()
    PawView(tint: .red).frame(maxHeight: .infinity, alignment: .bottom)
}
// 官方引荐写法
PawView(tint: isGood ? .green : .red)
    .frame(maxHeight: .infinity, alignment: isGood ? .top : .bottom)
结构身份的宿敌(AnyView)

了解完结构身份,咱们再来谈谈它的宿敌 – AnyView。咱们先来看看下面这段运用了 AnyView 的函数,这个函数需求回来一个单一类型,所以它用了 AnyView 来包装各个不同视图。这样就会导致 SwiftUI 内部无法看到代码的条件结构,只能看到一个 AnyView 的回来类型。由于 AnyView 隐藏了它所包装的一切视图的类型,也使代码可读性变差。

WWDC21 | Demystify SwiftUI

咱们能够进行一番优化,如下。相对于上面的代码罢了,下面的代码使得 SwiftUI 内部获取 some View 的结构不再是单一而是变得明晰。这儿应该留意的是要加@ViewBuilderbody 特点是默认(隐式)添加,可是咱们自定义的办法,需求自行添加 @ViewBuilder ,不然会报错。当然这儿运用 switch 会更直观一些。

WWDC21 | Demystify SwiftUI

所以咱们要尽量防止运用 AnyView

  • AnyView 运用太多通常会使代码更难阅读和理解;
  • AnyView 对编译器隐藏了静态的类型信息,导致一些有用的过错和正告不会提示;
  • AnyView 某些情况会导致功能下降。比较合适做法便是运用泛型,来保留静态类型信息。

下面咱们来看一下第二要素 LifeTime (生命周期)。

Lifetime(生命周期)

咱们人的生命周期是从出生到寿终,这期间是有酸甜苦辣的各种心情状况表达。视图也是如此,视图一旦被标识了身份,那它就存在一个生命周期,经过视图值的变化,视图在它的生命周期中也会有各种状况。下面图中有一个 bgView,在不同的时间点上有不同的视图值(color),相对应 bgView 在这些点上呈现的状况也不同。

这儿需求留意的是,咱们不能经过某个时间短的视图值来当作视图 bgView 的生命周期,视图的生命周期必须是由视图的身份决议的。当视图的身份发生变化或者视图被删除时,这就意味它的生命周期完毕。也便是说生命周期其实便是一个身份的持续时间,这个身份与视图相相关。且身份是仅有的,多个视图就不能共享一个身份。所以这也表现了身份标识的安稳性至关重要:不安稳的身份会导致更短的视图生命周期;而安稳的身份有助于提高功能,由于 SwiftUI 不需求一直为视图创立存储;

WWDC21 | Demystify SwiftUI

在上述视图生命周期中,咱们能够看到视图是能够更改其状况。例如咱们最开始运用的比如,当咱们滑动开关,该值由 true 变为 false 时,SwiftUI 会先保留旧视图值的副本,执行比较后,再决议是否更新视图。

struct SwitchView: View {
    @State var isOn: Bool = true
    var body: some View {
        Toggle("Switch is \(isOn ? "On" : "Off")", isOn: $isOn)
    }
}

视图的状况与生命周期是怎么相相关的呢?经过@State@StateObject 与视图身份相相关,如 isOn, 他们是耐久化存储视图状况的办法。在视图标识身份时,也便是第一次创立时分,SwiftUI 会为 @State@StateObject 分配内存中的存储。

Dependencies (依靠项)

依靠项与视图的联系

咱们先分析一下下面这段代码的视图结构。顶部有两个特点,一个是 dog(狗),一个是 treat(零食),这两个特点便是视图的依靠项, 除了 body 是主体,其他的特点都是依靠项。

WWDC21 | Demystify SwiftUI

咱们将代码转化成图表,咱们能够更直观看到,整个视图与依靠项的关系。

WWDC21 | Demystify SwiftUI

尽管上面图表结构是树型结构,可是视图与依靠项之间的关系并不仅仅如此,咱们添加一些依靠项,并让多个视图与他们相相关,就得到一张比之前更复杂的结构图(左下)。咱们重新排列它,以防止重叠线条,就能得到右下图结构,咱们称之为 “依靠图”。这个结构能协助 SwiftUI 判断哪些视图的 body 需求更新,哪些不需求更新。

WWDC21 | Demystify SwiftUI

当某个依靠项发生变化时,将会给一切的视图生成一个新的 body 值,然后把依靠项相相关的视图 body 值实例化,当然假如依靠变更不符合视图更新条件,对应的视图也不会更新。这个在咱们的 Lifetime(生命周期)中也讲到。

依靠项种类

除了一般结构体特点外,依靠项还包括以下几个特点包装器:

  • @Binding
  • @Environment
  • @State
  • @StateObject
  • @ObservedObject
  • @EnvironmentObject

由这些修饰的特点都被称为依靠项,可是前提是被视图引证。

参考文章

  1. 《Demystify SwiftUI》- Apple 官方
  2. 《关于 SwiftUI,看这一篇就够了》 – 梁启健
  3. 《从 SwiftUI 谈声明式 UI 与类型系统》- Cyandev(字节 iOS)
  4. 《WWDC NOTES: Demystify SwiftUI》- Federico Zanetello