首先,涉及一个性能原理:结构体比类更简单,更轻量。之所以名列前茅个说这个原因,是因为大多数都认为这是 SwiftUI 采用结构体的主要原因。其实,纵观全局,这只是原因之一。
一、SwiftUI用struct来表示view的原因
首先,涉及一个性能原理:结构体比类更简单,更轻量。之所以名列前茅个说这个原因,是因为大多数都认为这是 SwiftUI 采用结构体的主要原因。其实,纵观全局,这只是原因之一。
在 UIKit 中,所有的视图都继承自一个叫UIView的类,它有非常多的属性和方法 —— 背景颜色,布局约束,用于渲染的层,等等。还有更多诸如此类的属性,而每一个UIView和UIView的子类都有,因为这正是继承的工作方式。
通常这样也不会带来问题,但有一个特殊的子类UIStackView,它和 SwiftUI 里的VStack和HStack相似。在UIKi 里,出于使布局更简单的设计意图,UIStackView是一个不会被渲染的视图类型。但由于继承机制,尽管它不渲染,它也有那些包括背景颜色在内的各种用不上的属性。
在 SwiftUI 中,所有的视图都是细碎的结构体,创建开销几乎可以忽略。 想象一下:你创建了一个结构体,持有一个整数,整个结构体的大小只有——那个整数,再无其他。没有从父类、爷爷类、爷爷的爷爷类那里继承来的“意外财产”。它所包含的一切你都看得见。
得益于现代 iPhone 的能力,创建 1000 甚至 100,000 个整数只在眨眼之间。对于 SwiftUI 的 1000 个 view 或者 100,000 个 view。这个时间仍然成立。太快了,你都不必考虑它们。
不过,除了性能,用 struct 表示 view 还有其他重要原因:它强迫我们以一种更干净的方式隔离状态。类可以自由地修改它的值 —— 这可能导致更凌乱的代码,这样的话 SwiftUI 就无法通过某个值的变化来自动更新 UI 了。
通过创建不会跟随时间改变的视图,SwiftUI 鼓励我们迁移到一种可以更好地工作的设计方式:视图变简单,变“蠢”,它只做把数据变成 UI 的事情,而不是滋生出控制逻辑这样更“智能”的工作。
当你审视什么样的东西在 SwiftUI 中可以作为一个 view 的时候,你就会发现前面说的方式正在运作。我们用 Color.red 和 LinearGradient 作为视图 —— 一些存储非常简单数据的细碎类型。实际上,相对于把 Color.red 直接当成 view,你找不到更好的方案了。除了“把我的空间填满红色”,它没有携带其他任何多余的信息。
作为比较,你可以看下 Apple 的UIView文档。上面列出了200 多个UIView的属性和方法 ——不管子类需不需要,都拿着。
提示:如果你试图给你的 view 用上 class,那么代码要么编译不过要么就会崩溃。不要犹豫:用 struct 。
延伸阅读:
二、视图值树是什么
在 SwiftUI 中,视图是状态的函数。
开发者通过符合 View 协议的结构体来声明界面,SwiftUI 通过调用结构体实例的 body 获取对应的视图值。body 则根据用户的界面描述和对应的依赖(Source of truth)计算结果。
在 app 运行后进行名列前茅次渲染时,SwiftUI 将依据类型树按图索骥,创建类型实例,实例的 body 根据初始状态计算视图值,并组织成视图值树。需要创建哪些实例,则是根据当时的状态决定的,每次的状态变化都可能会导致最终生成的视图值树不同(可能仅是某个节点的视图值发生变化,也可能是视图值树的结构都发生了巨大的变化)。
当 State 发生变化后,SwiftUI 会生成一棵新的视图值树(Source of truth 没有发生变化的节点,不会重新计算,直接使用旧值),并同老的视图值树进行比对,SwiftUI 将对其中有变化的部分重新布局渲染,并用新生成的视图值树取代老的视图值树。
视图值树通常只保存当前布局、渲染所需的内容(个别情况下,会缓存少数不参与布局、渲染的视图值),在 app 的生命周期中,随着 State 的变化而不断地变化。