什么是 Structural Identity?
SwiftUI 通过结构身份(Structural Identity)判断新旧视图树中的同一个节点:
- 类型相同
- 在层级中的位置相同
- 祖先链的身份相同
只有当三者一致时,SwiftUI 才认为"这是老熟人",保留其内部 @State / @StateObject 等局部状态;否则旧节点被销毁,新节点重新创建 → 状态归零。
Update vs Redraw:先分清两个概念
术语 | 含义 | 是否一定刷像素 |
---|---|---|
Update | 重新初始化 View 值(执行 body ) |
❌ 仅 diff |
Redraw | 向 GPU 提交绘制指令 | ✅ 真正刷界面 |
Structural Identity 决定的是能否复用旧节点,从而影响Update 次数与状态生命周期;Redraw只在属性变化时发生。
经典踩坑:if-else
导致状态丢失
swift
struct ExampleView: View {
@State private var isOn = true
var body: some View {
VStack {
Text("Is on: \(isOn ? "yes" : "no")")
Button("Switch") { isOn.toggle() }
if isOn { // ← 条件分支
BottomViewOn() // ① 类型与位置在变
} else {
BottomViewOff() // ② SwiftUI 认为是两个**不同**节点
}
}
}
}
结果:
isOn
变化 → 分支切换 →BottomViewOn/Off
类型不同 → Structural Identity 失效 → 旧节点被销毁。- 两个子视图内部的
@State
/@StateObject
全部重置。
保持身份的两种策略
外部化状态(推荐)
把需要持久的属性提升到父视图或注入 Observable 对象:
swift
@StateObject private var bottomVM = BottomViewModel()
...
if isOn {
BottomViewOn(vm: bottomVM)
} else {
BottomViewOff(vm: bottomVM)
}
节点虽换,但状态由外部 VM 持有,不再依赖局部 @State
。
使用透明/位移技巧(ZStack + opacity)
swift
ZStack {
BottomViewOn()
.opacity(isOn ? 1 : 0)
.accessibilityHidden(!isOn)
BottomViewOff()
.opacity(isOn ? 0 : 1)
.accessibilityHidden(isOn)
}
- 两个子视图始终存在 → 类型 & 位置不变 → 身份保留。
- 仅改变视觉属性(opacity),无节点销毁 → 状态常驻。
代价:同时占用内存/渲染通道,适合轻量级视图。
列表中的身份:为什么 id:
如此重要
swift
List(users, id: \.id) { user in
RowView(user: user)
}
- 提供稳定且唯一的
id
后,SwiftUI 才能追踪同一数据项在插入/删除/移动后的位置。 - 用数组索引或可能重复的属性当
id
→ 身份错乱 → 出现"行内容错位"或"状态串台"。
性能提示:不要滥用 .id(UUID())
强制 reload
swift
MyComplexView()
.id(viewId) // 每次改 UUID → 节点被判定为新实例
-
确实能强制刷新,但会丢弃所有内部状态 & 重建整个子树。
-
只在你真正需要重置(retry、错误恢复)时使用;
日常状态更新应靠数据驱动,而非改身份。
一句话总结
"类型 + 位置 + 祖先" 三要素只要变了,SwiftUI 就认不出旧视图 → 状态清零。
想让数据常驻:
- 外部化状态(首选)
- 保持结构不动(ZStack/opacity 技巧)
- 给列表稳定 id
记住这三点,再不会被"莫名其妙丢状态"坑到。