这是我的 SwiftUI 学习笔记系列第一篇。
虽有 AI 助力开发,但若想指挥它写出优雅且有品位的代码,并在调试时透彻理解底层逻辑,仍需深度思考。在当下,学习一门语言,掌握语法已非首要,更重要的是建立一张知识地图。希望这个系列能绘就这张图。
作者初学SwiftUI,文中难免有疏漏或理解不到位之处,欢迎各位开发者指正探讨,我们共同进步。
SwiftUI 底层机制解密:你的 View 并不等于真正的视图
这几年,苹果一直在推动 SwiftUI。很多从 UIKit 转过来的 iOS 开发者,刚上手时都会觉得别扭。
你写了一个 Text,配上一个 @State,界面就会自动刷新;但有时只是改了下 if-else 的结构,原本正常的状态却突然丢了。
这类困惑,往往是因为很多人理解 SwiftUI 时,脑子里仍然装着 UIKit 的对象模型。想把 SwiftUI 用明白,先得换一套心智模型。
一、从对象到描述
在 UIKit 时代,视图,比如 UIView、UILabel,本质上都是对象,也就是 class 实例。它们存在于堆内存中,内部不仅有显示内容本身,还关联着图层、布局、响应链、约束、缓存等运行时状态。对象一旦创建出来,就会持续存在,直到你把它移除或释放。
SwiftUI 不一样。SwiftUI 里的 View 通常是结构体,也就是值类型。
当你写下:
Text("Hello")
这并不意味着屏幕上立刻多了一个真实的文本控件。更准确地说,你是在描述一段界面结构。这个描述本身很轻量,可以被频繁创建,也可以被频繁丢弃。
这也是为什么 View 协议最核心的部分是:
var body: some View
body 不是在返回一个早就存在的视图对象,而是在当前状态下重新计算出一份新的界面描述。SwiftUI 的工作方式可以概括为:
状态变了,body 重新计算;新的 View 描述生成后,框架再决定如何更新底层界面。
所以从模型上看,SwiftUI 更接近这样一种关系:
UI= f(State)
这里的 View 更像声明,而不是对象本身。
二、短暂的 View,与持久的状态系统
既然 View 会不断重建,那么问题来了:输入框里的内容、列表滚动位置、局部状态,这些东西到底存在哪里?
答案是:它们并不真正存放在那些每次都会重新生成的结构体 View 里。
更准确的理解方式是:你在代码里写出来的 View 树,是一层短暂的声明结构;而 SwiftUI 在底层维护着另一套持久存在的状态和依赖关系系统。外界常会用“渲染树”或 “Attribute Graph” 来粗略描述这部分机制。真正持续存在的状态,不在 View 结构体里。
比如:
@State private var count =0
count 看起来写在 View 里,但它的实际存储和生命周期管理,是由 SwiftUI 底层托管的。每次 body 重算时,SwiftUI 会根据当前视图的身份,把原先那份状态重新关联回来。
所以你看到的是“View 变了,但状态没丢”;更准确地说,是“界面描述被重建了,但底层托管的状态仍然被复用了”。
三、Diffing 的关键,不是内容,而是身份
既然每次生成出来的 View 都是新的值,那么 SwiftUI 怎么知道:
“这次的这个输入框”,是不是上一次那个输入框?
这就涉及 SwiftUI 的 Diffing,也就是差异比较机制,以及其中一个核心概念:Identity,标识。
SwiftUI 只有先判断“你还是不是原来那个节点”,才能决定是复用状态,还是销毁重建。
1. 默认情况下,SwiftUI 按结构认人
如果你没有显式指定身份,SwiftUI 通常会根据视图在层级中的位置和结构来判断它是不是“同一个东西”。
例如:
if isVertical { VStack { CounterView() }} else { HStack { CounterView() }}
表面上看,两边都是 CounterView。但对 SwiftUI 来说,这两个分支的结构并不一样。一个在 VStack 下面,一个在 HStack 下面,所在路径已经变了。
结果就是:当 isVertical 切换时,SwiftUI 很可能把它当成一个新的节点处理。原来挂在旧节点上的状态,也就跟着丢了。
这也是很多人第一次遇到“只是改了布局,状态怎么没了”的原因。
2. 只想换布局,不想换身份
如果你的目标只是切换布局方式,而不是替换整个视图身份,那就应该尽量避免写成两个完全不同的分支结构。
在 iOS 16 之后,AnyLayout 是一种更稳妥的写法:
let layout = isVertical ?AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())layout { CounterView()}
这里变的是布局算法,不是 CounterView 在树里的基本身份,因此状态更容易保留下来。
类似的思路也适用于很多条件修饰。比如:
.background(show ? Color.red : Color.clear)
通常就比用 if-else 去切换整段背景结构更平滑,因为它保留了更连续的视图形状。
3. 显式指定身份:.id(...)
有时候,仅靠结构位置还不够。这时你可以直接告诉 SwiftUI:“它是谁”。
.id(sessionID)
.id 本质上是在指定视图的身份边界。只要这个值变了,SwiftUI 就会把这个节点视为一个全新的视图。
这在某些场景下很有用。比如一个复杂表单,你想一次性清空所有局部状态,与其逐个重置 @State,不如给表单根节点绑一个 .id(sessionID)。当 sessionID 变化时,SwiftUI 会直接丢弃旧节点对应的状态,重新建立一套新的状态空间。
所以 .id 既可以用来稳定身份,也可以用来主动触发一次彻底重建。
四、修饰符不是改属性,而是在包结构
SwiftUI 里经常会写这样的链式代码:
Text("标签") .background(.yellow) .padding(10) .background(.blue)
很多人第一次看到结果时会疑惑:为什么最后的蓝色背景没有把前面的黄色盖掉?
因为 SwiftUI 的修饰符通常不是“原地修改”,而是返回一个新的 View,把原来的 View 包在里面。
可以把上面这段代码理解成这样一层层套起来:
- 最里面是
Text - 外面包一层黄色背景
- 再外面包一层
padding - 最外层再包一层蓝色背景
所以最终看到的效果不是“蓝色覆盖黄色”,而是黄色背景贴着文字,外面被 padding 撑开,再由蓝色背景包住更大的区域。
这也是 SwiftUI 一个很重要的特点:顺序就是结构,结构决定结果。
五、SwiftUI 为什么这么依赖静态类型
SwiftUI 还有一个初看有点别扭的地方:它非常依赖静态类型。
比如你可能直觉上会想写:
var myView: some View {if Bool.random() { return Text("A") } else { return Circle() }}
这段代码直接写通常行不通,因为 some View 并不表示“任意一个符合 View 的类型都可以”,它表示的是:一个具体但对外隐藏的单一类型。
Text 和 Circle 是两个不同的类型,不能直接作为同一个 some View 返回。
这时 @ViewBuilder 的作用就出现了。它会把这类条件分支组合成一个统一的返回类型,让整个表达式仍然能落在静态类型系统里。
为什么 SwiftUI 要这么设计?因为静态类型能给框架大量编译期信息。视图的层级、分支、组合关系,很多内容在编译阶段就已经被编码进类型结构里了。这样一来,SwiftUI 在更新 UI 时可以少做很多运行时判断,也更容易进行高效比较和局部更新。
这里不必把它理解成某种“完全不遍历”的神奇优化。更准确的说法是:静态类型给了 SwiftUI 更强的结构信息,让它能更高效地做比较和更新。
六、动态内容怎么处理:ForEach 和 AnyView 分别解决什么问题
那如果我有一组动态数据,比如 100 条列表项,SwiftUI 怎么处理?
这就是 ForEach 的意义。
ForEach 不是普通的 for 循环,它是一个带有身份语义的视图容器。你给它一组数据,再给每个元素一个稳定的 id,SwiftUI 才能在数据变化时判断:哪些项是新增的,哪些是删除的,哪些只是顺序变了。
ForEach(items, id: \.id) { item in Text(item.title)}
这里最关键的不是“循环”,而是“稳定身份”。
至于 AnyView,它解决的是另一类问题:当你确实需要在运行时把不同类型的视图装进同一个容器里时,可以用它来做类型擦除。
例如:
let view: AnyView= condition ? AnyView(Text("A")) : AnyView(Circle())
这当然能用,但代价也很直接:你把原本可见的静态类型信息抹掉了。SwiftUI 因此失去了一部分优化空间。
所以 AnyView 不是不能用,而是不应该变成默认手段。它更适合作为局部适配,而不是组织界面的常规方式。
总结
你在代码里写下的 View,本质上是一份基于当前状态生成出来的界面描述。它可以被反复创建,也可以被反复丢弃;真正持续存在的,是 SwiftUI 底层托管的状态、依赖关系,以及它与平台渲染系统之间的映射。
一旦接受这个模型,很多表面上“奇怪”的现象就会变得自然:
- 为什么
body 会反复执行 - 为什么结构一变,状态可能会丢
- 为什么修饰符顺序会影响结果
- 为什么
ForEach 需要稳定的 id - 为什么
AnyView 会让 SwiftUI 更难优化
SwiftUI 不是在“操作视图对象”,而是在持续根据状态重新描述界面,再由框架决定如何把这些描述落实到底层系统中。
这才是声明式 UI 的核心。