为什么 UIKit 程序员总问"我的状态去哪了?"
特性 | UIKit | SwiftUI |
---|---|---|
视图定义与生命周期 | 视图为类(Class),生命周期明确,长期驻留内存 | 视图为值类型(Struct),每次刷新生成新实例 |
状态保存方式 | 状态保存在视图对象内部 | Struct 销毁后,状态需由外部系统(如 ObservableObject、@State 等)托管 |
SwiftUI 提供了一堆 Property Wrapper 来"假装"状态还在视图里,核心就是 @State
。
@State 到底做了什么?(4 步流水线)
SwiftUI 把一次刷新拆成 4 个微观阶段:
-
Invalidation(打脏标)
对用到的属性插 依赖旗标;值改变时插旗为 dirty。
-
Recompute(重算 body)
只重算脏旗波及的 body;没读到值的 State 直接跳过。
-
Diffing(结构差异)
旧的 View 树 vs 新的 View 树,找出最小集合。
-
Redraw(GPU 提交)
Core Animation 仅把真正改动的图层提交给 GPU。
Attribute 系统:给"视图模板"注水
swift
struct DemoView: View {
// 1️⃣ 在视图首次出现时,SwiftUI 为其创建一个持久化的存储槽位
@State private var threshold: CGFloat = 50.0 // ← 生成一个 attribute
var body: some View {
VStack { // ← 生成一个 attribute
Button("改变") {
threshold = 41.24 // 2️⃣ 写入新值 -> 生成 Transaction
}
Text("当前阈值 \(threshold)") // 3️⃣ 读取值 -> 建立依赖
}
}
}
- Transaction:同一"事件循环"里所有 State 变化打包成一次事务。
- Cascade Flag:只要
threshold
被打脏,所有读过它的 attribute 都会被连锁打脏。 - Rule:body 里没读到 = 不 recomputed。官方 Instrument 里会显示
body(skipped)
。
身份稳定:为什么"同一个"视图才能保持 State
swift
// ❌ 错误示范:切换分支时 struct 类型相同,但身份不同 -> State 丢失
struct MyMusic: View {
@State private var rockNRoll = false
var body: some View {
VStack {
if rockNRoll {
MusicBand(name: "The Rolling Stones") // 新身份
} else {
MusicBand(name: "The Beatles") // 另一个身份
}
}
}
}
// ✅ 正确姿势:保证身份稳定(使用相同视图,只改参数)
struct MyMusic: View {
@State private var rockNRoll = false
var body: some View {
MusicBand(name: rockNRoll ? "The Rolling Stones" : "The Beatles")
}
}
口诀:
"同一视图,不同入参" 用参数传值;
"不同视图" 用 if/else
就会换身份,State 清零。
Body 重算粒度实验
只写不读 → 跳过
swift
struct MovieDetail: View {
let movie: Movie
@State private var favoriteMovies: [String] = []
var body: some View {
VStack {
Button("加收藏") {
favoriteMovies.append(movie.name) // 只写
}
}
}
}
Instrument 显示:MovieDetail.body [skipped]
读写 → 重算,但子视图可跳过
swift
var body: some View {
VStack {
HStack { // 👈 重算,因为读 favoriteMovies
Text(movie.name)
Image(systemName: favoriteMovies.contains(movie.name) ? "star.fill" : "star")
}
Artwork() // 👈 没传参,不 recomputed
Synopsis()
Reviews()
}
}
经验:把"纯展示"拆成无参子视图就能躲过重算。
Equatable:手动告诉 SwiftUI"别算我"
swift
struct FlightDetail: View, Equatable {
let flightNumber: Int
let isDelayed: Bool
// 自定义相等:只看航班号
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.flightNumber == rhs.flightNumber
}
var body: some View {
VStack {
Text("航班 \(flightNumber)")
Text(isDelayed ? "延误" : "准点")
}
}
}
- 若 struct 全是 POD 类型(Int/Bool...),SwiftUI 会跳过你的
==
,直接按位比较。 - 想让自定义相等生效:
- 包一层
EquatableView(content: FlightDetail(...))
- 或者
.equatable()
修饰符。
- 包一层
@Observable vs ObservableObject:从"对象级"到"属性级"
特性 | Combine(旧) | Observation(新) |
---|---|---|
监听机制 | 监听 objectWillChange 发布者 |
监听具体属性的 KeyPath 变化 |
更新范围 | 任意 @Published 属性修改触发整个 body 重算 |
仅读取了被修改属性的 body 部分重算 |
属性包装器要求 | 需通过 @StateObject /@ObservedObject 管理可观察对象 |
可直接使用 @State var model = MyModel() 声明模型 |
swift
@Observable
final class Phone {
var number = "13800138000"
var battery = 100
}
struct Detail: View {
let phone: Phone // 无需 ObservedObject
var body: some View {
Text("电池 \(phone.battery)") // 只当 battery 变才重算
}
}
扩展场景:把知识用到"极端"界面
- 万级实时股票列表
- Model 用
@Observable
把price
单独标记; - 行视图实现
Equatable
仅对比symbol
+price
; - 收到 WebSocket 推送时只改
price
,其余字段不动 → 一行只重算自己。
- 复杂表单(100+ 输入框)
- 把每个字段拆成独立子视图;
- 用
@FocusState
+@Observable
FormModel,保证敲一个字只重算当前TextField
; - 提交按钮用
.equatable()
锁定,输入过程不刷新。
- 大图轮播 + 陀螺仪
@State
保存偏移;- 用
TimelineView
按帧读陀螺仪,但把昂贵的图片解码放到后台Task
; - 仅当图片索引变化才改
Image(source)
,避免每帧 diff 大图。
个人总结:从"魔法"到"可预测"
SwiftUI 的刷新机制看似黑盒,实则高度 可确定:
"谁依赖,谁重算;谁相等,谁跳过;谁不变,谁不绘。"
把它当成一个依赖追踪引擎而非"UI 库",就能解释所有现象:
- 状态放对位置(身份稳定);
- 依赖剪到最细(读多少算多少);
- 比较给到提示(Equatable/Observable);
- 性能用 Instrument 量化(Effect Graph + Core Animation)。
掌握这四步,SwiftUI 不再是"玄学",而是可推导、可度量、可优化的纯函数式渲染管道。
参考资料 & 工具
- 官方文档:SwiftUI State
- WWDC 23:Discover Observation in SwiftUI
- Xcode 15 ▸ Instruments ▸ SwiftUI template ▸ Effect Graph
- SwiftUI 重绘系统深入了解:属性、重新计算、差异和观察