在 SwiftUI 中,body 是一个纯函数属性。它的唯一职责是描述 UI 的当前快照,而不是执行任何“动作”。
如果你在 body 内部执行副作用(如修改变量、发起请求、注册通知),你会进入一个非确定性的领域,产生一系列极难调试的非直觉 Bug。
1. “永不停止”的死循环(Infinite Update Loop)
这是最经典的非直觉 Bug。
-
现象:App 运行几秒后手机开始发烫,或者 CPU 占用率直接飙升到 100%,UI 却没有任何响应。
-
成因:
- SwiftUI 调用
body来渲染视图。 - 你在
body里修改了一个@State变量(副作用)。 State的改变通知 SwiftUI:“数据变了,请重新渲染”。- SwiftUI 再次调用
body。 - 重复步骤 2,陷入死循环。
- SwiftUI 调用
-
非直觉点:有时候这种死循环不是立即发生的。可能只在某个子视图出现时,或者在特定的环境变化(如横竖屏切换)时才被触发。
2. 逻辑执行次数的“不可预测性”
你可能认为 body 只在视图出现时跑一次,但事实并非如此。
- 现象:你在
body里写了一个print("Log Action")或者计数器,发现它有时打印 1 次,有时打印 5 次,甚至在用户什么都没操作时也在打印。 - 成因:SwiftUI 为了优化渲染,可能会在后台多次调用
body来进行 Diff 运算,或者因为父视图、环境值(Environment)的微小抖动而重绘。 - 非直觉 Bug:如果你在
body里通过Network.request()发起请求,你会发现服务器收到了成倍的重复请求,导致数据混乱或触发频率限制。
3. 消失的异步回调(The Ghost Callbacks)
如果你在 body 里创建并启动一个异步任务:
- 现象:由于
body会被频繁重新评估,旧的异步任务可能还在运行,新的任务又被开启。 - 非直觉 Bug:当异步结果返回并尝试更新 UI 时,它可能是在操作一个已经被丢弃的视图闭包中的变量。这会导致 UI 显示过时的数据(Stale Data),或者出现多个网络回调竞争修改同一个状态,产生极其随机的 UI 闪烁。
4. 视图 Identity 的破坏
某些副作用(如在 body 内部生成随机数 ID)会破坏 SwiftUI 的视图识别机制。
Swift
var body: some View {
// ❌ 严重错误:每次重绘 ID 都会变
let id = UUID()
Text("User").id(id)
}
- 非直觉 Bug:你会发现视图的动画彻底失效了。原本应该平滑移动的组件,现在变成了生硬的消失和出现。这是因为每次刷新 SwiftUI 都认为这是一个全新的视图,从而放弃了增量动画,转而进行全量重绘。
5. 防御式编程:副作用的“安家之地”
为了规避上述问题,副作用必须被包裹在受控的生命周期钩子或闭包中:
| 副作用类型 | 推荐放置位置 | 理由 |
|---|---|---|
| 初始加载 | .task 或 .onAppear | 保证在视图生命周期特定的节点只执行受控次数。 |
| 用户交互 | Button(action: { ... }) | 只有在明确的事件触发时才执行。 |
| 状态联动 | .onChange(of: { ... }) | 明确监听某个值的变化,而非随 body 无感触发。 |
| 异步清理 | .task 内的 await 逻辑 | 利用 Swift 并发机制,在视图销毁时自动取消任务。 |
总结
body 应该是“静止的影评”,而不是“正在进行的电影”。 如果你在影评里试图修改电影的结局,整个剧场都会乱套。