第 1 章 视图是值,不是控件
body 为什么会被反复调用、some View 到底藏了什么、视图树怎么被差分——以及为什么 body 里不能有副作用
6 分钟阅读
这不是又一份"从 View 讲到 NavigationStack"的教程。你的 Swift 基础部分 第七篇(第 40–47 章)已经把 SwiftUI 的概念铺完了,SwiftUI 官方文档整理 则是一本按 API 分类的字典。
这一份走第三条路:只讲那些必须先想通的事,并且每讲一个规则,尽量给一个实测数字。
SwiftUI 的表面门槛极低——Text("你好") 就出字,半小时能做出能跑的界面。真正的门槛在下面这些事上,它们互相咬合,缺一个都会卡住:
| 章 | 一句话 | 不学会怎样 |
|---|---|---|
| 01 视图是值 | View 是结构体,body 是计算属性 | 会把视图当控件去"改",然后奇怪为什么改了没用 |
| 02 状态是源头 | 数据变了界面才会变,反过来不行 | 写出"列表点了不刷新",然后卡三天 |
| 03 布局是一次谈判 | 父提尺寸 → 子报尺寸 → 父定位子 | 永远在靠猜加 frame,改一处崩三处 |
| 04 对齐与容器 | 对齐是"和谁对齐",不是"往哪儿挪" | 两个高度不同的图怎么都对不齐 |
| 05 动画的本质 | 动画是两个状态值之间的补间 | 写了 .animation 却不动,或者全都在乱动 |
| 06 手势与身份 | 手势改状态,身份决定视图是否被复用 | 拖拽卡顿、列表复用出错、动画莫名其妙 |
| 07 组件化 | 重复的修饰符/布局/交互,各抽成一种东西 | 同一个样式抄 30 遍,改一次漏三处 |
| 08 列表与导航 | 身份决定行状态属于谁;页面是一叠值 | 删一行后别的行状态串了;导航没法编程控制 |
| 09 数据落地 | 用 enum 建三态;I/O 别进视图 | 用三个布尔变量描述一件事,非法状态满天飞 |
| 10 性能与调试 | body 调用次数不重要,里面干了什么才重要 | 凭直觉优化,改了半天没变快 |
| 11 可访问性与适配 | 字号、旁白、颜色、动效偏好都是需求 | 一部分用户根本用不了你的界面 |
| 12 绘制 | Shape 描述路径,Canvas 批量绘制 | 系统没提供的图形就做不出来 |
| 13 视觉特效 | 材质、混合、visualEffect 的正确用法 | 自己 blur 冒充毛玻璃,又贵又不像 |
SwiftUI 是视觉框架,但"看起来是什么样"很难用嘴说清。所以本教程里凡是涉及尺寸、对齐、绘制的结论,都附了实测数字。
测量方法:在无窗口模式下加载视图(NSHostingController),然后用 GeometryReader 读取框架真实分配的尺寸和位置。
⚠️ 这里有个方法论教训值得单独说:最初我用"把视图渲染进位图、逐像素扫描色块边界"的办法,结果得出了错误结论——因为位图只能反映画布范围内的内容,一旦视图溢出容器,我就把"可见的那一段"当成了"实际尺寸",于是误以为 SwiftUI 会压缩固定宽度的视图。改用 GeometryReader 读真实分配值之后才发现:它根本不压缩,是让内容溢出。
第 03 章那一节保留了这次纠错的完整记录,因为这个错误本身很有教学价值——它示范了"用一个会骗人的测量手段去验证结论"有多危险。
无界面环境能做的事有边界。以下几类断言我明确标注了是"推导 / 文档化行为"而不是实测:
| 内容 | 位置 | 为什么测不了 |
|---|---|---|
| 列表身份选错会让行状态串位 | 第 08 章 | 需要真实的数据变更事件;每个 NSHostingController 都是独立的树 |
手势被系统取消时 onEnded 不触发 | 第 06 章 | 手势需要真实事件流(正文给了 SDK 里 reset 闭包的机制证据) |
.fixedSize() 的确切效果 | 第 03 章 | 测出来的数字不稳定,所以不给数字 |
| 视觉效果的"好看程度" | 第 12、13 章 | 需要人眼判断 |
💭 标出来比含糊过去好。你如果发现哪一处描述与你的实际结果不符,那很可能是这些地方——贴给我,我改。
@Observable 与 visualEffect 是 iOS 17、glassEffect 是 iOS 26)。View + #Preview,不含 @main——App 的入口(@main struct MyApp: App)在 Xcode 模板建的 App 文件里,让它的 WindowGroup 指向正文的主视图即可(见第 1 章 §1.6)。t.swift,swiftc t.swift -o t && ./t(或 swift t.swift)。这些片段是顶层代码,不需要 @main;反过来,一旦加上 -parse-as-library(或改用多文件编译),就必须写 @main——两种入口写法互斥,用错哪个都会报错,对照表见第 1 章 §1.2。// prints: 标注;⚠️ 易错点,🛑 编译错误示例,🚧 能编译但有代价,🔥 高频重点,🔬 实测,🔤 名词解释(第一次出现的术语在这里补定义),📦 运行方式提示。| 你在做什么 | 去哪一份 |
|---|---|
| 第一次学 SwiftUI,或者被布局/动画卡住了 | 本教程,按顺序读,第 3 章最值得慢读 |
| 想知道"这个东西的完整概念体系" | Swift 基础部分第七篇,第 40–47 章 |
| 查某个 API 的签名和用法 | SwiftUI 官方文档整理 |
| 写 Swift 语法忘了怎么写 | Swift 速查表 |
| 程序崩了、结果不对 | Swift 血泪速查 |
第一幕:把模型立起来
body 为什么会被反复调用、some View 到底藏了什么、视图树是怎么被差分的@State / @Binding / @Observable / @Environment 的选择判据,以及"不刷新"bug 的定位法第二幕:布局(最容易劝退的部分)
frame 的四层含义、Spacer 与 maxWidth 的实测分配规则alignment 是与谁对齐、overlay 的尺寸与裁剪语义、Grid 与自定义 Layout第三幕:动起来
withAnimation 与 .animation(_:value:) 的分工、为什么有的属性不动@GestureState 与 @State 的分工、拖拽/缩放/旋转、身份如何决定视图的命运第四幕:工程化
ViewModifier、泛型容器与 @ViewBuilder、Style 协议、PreferenceKeyList 三种写法、身份的实战后果、增删改查、NavigationStack 路径、弹窗家族.task 与 .task(id:)、网络请求、从 @AppStorage 到 SwiftData第五幕:质量
body 调用次数为什么是误导性指标、_printChanges、Lazy 容器的边界第六幕:表现力
Shape 与 Path、stroke 与 strokeBorder 的实测差异、四种渐变、CanvasvisualEffect、Liquid Glass💭 一句话建议:如果你时间很紧,只读第 3 章。布局是 SwiftUI 里唯一"不看数字就学不会"的部分,也是绝大多数界面问题的根源。
body 为什么会被反复调用、some View 到底藏了什么、视图树怎么被差分——以及为什么 body 里不能有副作用
@State / @Binding / @Observable / @Environment 的选择判据,以及「改了数据界面不刷新」的四种病因与定位方法
父提尺寸→子报尺寸→父定位子:用实测数字拆开 SwiftUI 布局协议、frame 的四层含义、Spacer 与 maxWidth 的分配规则
alignment 是「和谁对齐」而不是「往哪儿挪」:用实测坐标讲清 HStack 对齐、overlay 与 background、ZStack、Grid 与自定义 Layout
动画 = 框架反复给中间值:withAnimation 与 .animation(_:value:) 的分工、为什么有的属性不动、transition 与身份的关系
用 @GestureState 还是 @State:手势的两种状态分工;以及身份如何决定视图的命运——状态何时保留、何时被重置
把重复的修饰符、重复的布局、重复的交互各抽成一种东西:ViewModifier、@ViewBuilder 容器、Style 协议,以及怎么选
List 的三种写法、身份在滚动视图里的真实后果、增删改查、NavigationStack 的路径与 programmatic 导航、弹窗家族的分工
把「加载中/成功/失败」建对模;.task 与 .task(id:) 的分工;网络请求怎么写不搞乱 body;从 @AppStorage 到 SwiftData 的持久化选择
body 调用次数为什么是误导性指标、_printChanges 怎么用、Lazy 容器的真正边界、以及 SwiftUI 里真正值得优化的四件事
把字号调到最大、打开旁白、切换深色模式之后,你的界面还站得住吗——动态字体、语义标签、颜色与动效偏好、尺寸类适配
自己画系统没提供的图形:Shape 与 Path 的坐标系、stroke 与 strokeBorder 的实测差异、四种渐变、Canvas 的命令式绘制与性能边界
Material 的层次关系、blur 与 shadow 的代价、混合模式的组合、visualEffect 如何在不用 GeometryReader 的前提下做滚动特效,以及 Liquid Glass