12-23.【SwiftUI】 View 的 body 里做副作用可能带来哪些非直觉 Bug?

5 阅读3分钟

在 SwiftUI 中,body 是一个纯函数属性。它的唯一职责是描述 UI 的当前快照,而不是执行任何“动作”。

如果你在 body 内部执行副作用(如修改变量、发起请求、注册通知),你会进入一个非确定性的领域,产生一系列极难调试的非直觉 Bug。


1. “永不停止”的死循环(Infinite Update Loop)

这是最经典的非直觉 Bug。

  • 现象:App 运行几秒后手机开始发烫,或者 CPU 占用率直接飙升到 100%,UI 却没有任何响应。

  • 成因

    1. SwiftUI 调用 body 来渲染视图。
    2. 你在 body 里修改了一个 @State 变量(副作用)。
    3. State 的改变通知 SwiftUI:“数据变了,请重新渲染”。
    4. SwiftUI 再次调用 body
    5. 重复步骤 2,陷入死循环。
  • 非直觉点:有时候这种死循环不是立即发生的。可能只在某个子视图出现时,或者在特定的环境变化(如横竖屏切换)时才被触发。


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 应该是“静止的影评”,而不是“正在进行的电影”。 如果你在影评里试图修改电影的结局,整个剧场都会乱套。