| name | swiftui-debugging |
| description | 诊断并修复 SwiftUI 渲染性能问题,包括不必要的 body 重新求值、缓慢或卡顿的视图、滚动/列表/网格的抖动、视图标识 bug、状态丢失、`Self._printChanges()` 输出过多、`@Observable` 与 `ObservableObject` 的观察范围、`AnyView` 性能顾虑,以及 SwiftUI `body` 内部的昂贵工作。 |
SwiftUI 调试
用本技能把 SwiftUI 性能抱怨变成一个小的证据循环:识别改了什么、确认 SwiftUI 是在重建还是仅仅在重新求值视图,然后应用最窄的修复。
工作流程
-
复现或定位症状。
- 编辑前先搜索被点名的视图、模型、列表/网格或修饰符链。
- 只有当目标视图或交互无法从仓库推断时,才请求最小复现。
- 优先使用性能分析器 trace、
_printChanges() 输出、Console 日志或清晰的交互路径,而非猜测。
-
选择诊断路径。
- 意外的 body 调用或嘈杂的
_printChanges():阅读 references/body-reevaluation.md。
- 状态重置、重复的
onAppear、诡异动画、焦点丢失或滚动重置:阅读 references/view-identity.md。
- 缓慢的列表、网格、滚动视图,或大量条目的初始加载:阅读
references/lazy-loading.md。
AnyView、在 body 中创建对象、昂贵的排序/过滤、过大的图片或重复的状态写入:阅读 references/common-pitfalls.md。
-
在大范围重写之前先做轻量的插桩。
- 在最小的可疑视图中使用
let _ = Self._printChanges()。
- 当代码检查无法解释开销时,使用 Instruments 的 SwiftUI 模板。
- 在昂贵路径周围使用
os.Logger 或 os_signpost 做自定义计数。
-
通过减少失效、保持标识或搬移工作来修复。
- 把状态下移到真正使用它的视图。
- 拆分大视图,使不相关的状态变化不会重新求值昂贵的子树。
- 为
ForEach 和 .id() 使用稳定的标识符。
- 对大集合用
List 或惰性 stack/grid 替换急切容器。
- 把繁重的格式化、解码、排序、过滤和图片准备移出
body。
-
验证症状,然后清理临时插桩,除非用户希望保留。
快速分诊
| 症状 | 首要嫌疑 | 参考 |
|---|
不相关的值改变时 body 运行 | 过度观察、父级重建、状态放得太高 | body-reevaluation.md |
_printChanges() 显示 @identity changed | 不稳定的 .id()、条件结构变化 | view-identity.md |
| 滚动视图一次创建数百行 | ScrollView 内的 VStack/HStack,没有惰性容器 | lazy-loading.md |
相同内容的 onAppear 重复 | 标识抖动或惰性单元回收 | view-identity.md、lazy-loading.md |
| 在字段中输入更新整个屏幕 | 宽泛的 ObservableObject、状态存得太高 | body-reevaluation.md |
| 初始加载或动画卡顿 | body 中的昂贵工作、图片解码、急切布局 | common-pitfalls.md |
| 动态视图类型周围的 diff 看起来很差 | AnyView 或被擦除的分支 | common-pitfalls.md |
有用的工具
Self._printChanges()
var body: some View {
let _ = Self._printChanges()
}
把常见输出解读为:
| 输出 | 含义 |
|---|
@self changed | 父级重建了这个视图值。检查父级失效和输入相等性。 |
@identity changed | SwiftUI 销毁并重建了视图。检查 .id()、ForEach ids 和分支。 |
_property changed | 某个存储属性、state、binding 或环境值发生了变化。 |
Instruments
使用 Xcode 的 SwiftUI 模板检查 View Body 计数、View Properties 变化和 Core Animation 提交。按计数或时长排序,复现慢交互,把编辑聚焦到最热的视图类型上。
自定义日志
用 os.Logger 做快速的本地计数,或在需要与 Instruments 时间线关联时用 os_signpost。
审查检查清单
- 没有不稳定的
.id() 值,如 UUID()、Date()、随机值或可变的数组索引。
ForEach 使用稳定、唯一的模型标识符。
- 不把条件分支仅仅用于在同一视图上切换修饰符。
- 大集合使用
List、LazyVStack、LazyHStack、LazyVGrid 或 LazyHGrid。
- 视图只观察它实际显示或修改的模型属性。
- 当部署目标允许时,优先使用
@Observable 而非宽泛的 ObservableObject。
- formatter、decoder、predicate、正则、view model 和图片解码不在
body 内部创建。
- 昂贵的过滤、排序和布局计算在变化时运行、放在缓存状态中,或在适当时脱离主 actor。
- 临时的
_printChanges()、print 日志和 signpost 在最终交付前被移除或明确有意保留。
API 可用性
| API / 技术 | 最低平台 |
|---|
Self._printChanges() | iOS 15 / macOS 12 时代的 SwiftUI 诊断 |
ObservableObject | iOS 13 / macOS 10.15 |
@Observable | iOS 17 / macOS 14 |
LazyVStack、LazyHStack、LazyVGrid、LazyHGrid | iOS 14 / macOS 11 |
.id() | iOS 13 / macOS 10.15 |
TimelineView | iOS 15 / macOS 12 |
| Instruments SwiftUI 模板 | Xcode 14+ |
参考
references/body-reevaluation.md:依赖失效、_printChanges()、观察范围和状态放置。
references/view-identity.md:结构标识、.id()、ForEach、分支标识和状态丢失。
references/lazy-loading.md:急切与惰性容器、列表/网格选择、滚动性能和惰性陷阱。
references/common-pitfalls.md:AnyView、对象分配、昂贵的 body 工作、图片解码、冗余状态和隐藏视图。
references/upstream-license.md:源材料的上游归属与 MIT 许可证声明。