这是 SwiftUI 的“宪法级规则” 。
一句话结论
body必须是纯函数,因为 SwiftUI 会把它当成一个“可随时、可重复、可并发调用的 UI 描述器”,只有纯函数才能保证 diff、调度和并发是安全且可预测的。
换句话说:
UI = f(State)
如果 f 不纯,整个 SwiftUI Runtime 就无法成立。
一、SwiftUI 对 body 的“隐含假设”
SwiftUI 在设计时 默认假设:
body可以被调用任意多次- 调用时机 不可预测
- 可能在 不同线程(尤其是未来)
- 调用结果 只取决于输入状态
只有在这些假设成立时,SwiftUI 才能:
- 做差分(diff)
- 合并更新(coalescing)
- 跳过不必要刷新
- 并发计算 View 树
二、什么是“纯函数”?(SwiftUI 语义)
在 SwiftUI 里,body 的纯函数含义是:
✅ 允许
- 读取 State / Binding
- 进行同步、确定性的计算
- 返回 View 描述
❌ 不允许
- 修改 State(直接或间接)
- 启动副作用(网络、IO、定时器、通知)
- 依赖外部可变全局状态
- 依赖调用次数 / 调用顺序
三、如果 body 不是纯函数,会发生什么?
❌ 示例:在 body 里改状态
var body: some View {
count += 1 // ❌
return Text("(count)")
}
会发生什么?
body 调用 → count 改变
→ State 改变 → 触发 body 再次调用
→ 无限循环(甚至 UI 卡死)
❌ 示例:在 body 里发请求
var body: some View {
fetchData() // ❌
return Text("Loading")
}
后果:
- body 被多次调用
- 请求被反复触发
- 网络风暴 / 数据错乱
- 调试几乎不可能
四、为什么 SwiftUI 需要这种“极端自由”?
1️⃣ 为了 Diff 算法成立
SwiftUI 的核心是:
旧 View 描述
vs
新 View 描述
→ 最小 UI 更新
如果 body 有副作用:
- diff 前后结果不可预测
- 无法判断“是否真的变了”
2️⃣ 为了合并更新(coalescing)
SwiftUI 会这样做:
State A 改变
State B 改变
→ 合并
→ 调用一次 body
如果 body 有副作用:
- 合并顺序影响行为
- 行为依赖调度策略(不稳定)
3️⃣ 为了未来并发 / 预计算
SwiftUI 已经在做:
- 在后台线程计算 View 树
- 推迟/提前计算 body
如果 body 不是纯函数:
- 并发执行会产生 race
- 主线程安全无法保证
五、那副作用应该放哪?
SwiftUI 给了明确的“副作用容器” :
✅ 正确位置
| 场景 | API |
|---|---|
| 首次出现 | .onAppear |
| 状态变化 | .onChange(of:) |
| 异步任务 | .task {} |
| ViewModel | ObservableObject |
| TCA | Effect / Reducer |
这些 API 的共同点是:
它们不参与 View diff,只参与“生命周期事件”
六、为什么这和 TCA / Redux 完美契合?
因为两者共享同一个数学模型:
Action → State → View
- View = 纯函数
- Reducer = 纯函数
- Effect = 副作用容器
这让你可以:
- 时间旅行
- 快照测试
- 完全重放 UI
七、和 UIKit 的本质区别
| UIKit | SwiftUI |
|---|---|
| View 是对象 | View 是值 |
| UI 手动更新 | UI 自动 diff |
| 生命周期驱动 | 状态驱动 |
| 副作用到处都是 | 副作用集中 |
UIKit 可以容忍不纯,因为你手动控制一切;
SwiftUI 不能容忍不纯,因为 Runtime 在帮你做一切。
最终一句话总结
SwiftUI 的
body必须是纯函数,不是“代码风格要求”,而是 SwiftUI Runtime 能够存在、扩展和并发的前提条件。
如果 body 里做了副作用,会带来哪些非直觉性 Bug 或性能问题?
一、最致命的一类:隐形无限循环(但你看不到 while)
❌ 例子:body 里改 State
var body: some View {
if isLoggedIn == false {
isLoggedIn = checkLogin() // ❌
}
return Text("Hello")
}
实际发生的事
body 执行
→ 修改 State
→ SwiftUI 标记 View dirty
→ 再次调用 body
→ 再次修改 State
→ …
为什么“非直觉”?
- 你 没写循环
- Xcode 不报错
- CPU 占用飙升
- UI 偶尔卡死,偶尔恢复
👉 这类 bug 在真机 / Release 模式 更容易出现。
二、请求风暴(你只写了一行网络代码)
❌ 例子:body 里发请求
var body: some View {
loadUser() // ❌
return Text(user.name)
}
非直觉后果
-
body被调用的次数:- 首次渲染
- 每个 State 改变
- 父 View 刷新
- 环境变化(暗黑模式 / 动态字体)
-
每次 → 一个请求
你看到的症状
- 网络请求数远超预期
- 后端日志异常
- 数据偶尔被覆盖
- QA 无法稳定复现
三、 “幽灵状态”Bug(最可怕)
这是 SwiftUI 新手最崩溃的一类。
❌ 例子:body 里注册观察者 / timer
var body: some View {
NotificationCenter.default.addObserver(...) // ❌
return Text("Hello")
}
实际发生的事
- body 被调用多次
- 观察者被注册 N 次
- 你只移除了一次(甚至没移除)
你看到的症状
- 回调触发次数越来越多
- UI 行为越来越怪
- 内存慢慢上涨
- Instruments 看不到明显泄漏
👉 这不是内存泄漏,是“逻辑泄漏” 。
四、性能地雷:重复昂贵计算
❌ 例子
var body: some View {
let result = heavyCompute(data) // ❌
return Text(result)
}
非直觉点
-
body不是“只在需要时调用” -
它可能在:
- 滚动中
- 父 View 状态变化
- 环境值变化
被反复调用
表现
- 滚动卡顿
- CPU 占用异常
- 性能问题只在复杂页面出现
五、并发灾难(未来必现)
SwiftUI 已经在并行计算 View 树,只是你现在“刚好没踩中”。
❌ 例子:body 里访问共享可变对象
var body: some View {
sharedCache.update() // ❌
return Text("OK")
}
后果
- data race
- 状态随机错乱
- Debug 没问题,Release 崩
👉 这类 bug 在多核设备 / 新系统更明显。
六、Identity 破坏:ViewModel / State 重建地狱
❌ 例子:body 里 new ViewModel
var body: some View {
let vm = ViewModel() // ❌
return ChildView(vm: vm)
}
后果
- 每次 body → 新 VM
- 网络请求重新发
- State 丢失
.onAppear行为异常
你会感觉:
“SwiftUI 好像随机地重置了我的页面”
其实是你在 body 里破坏了 identity。
七、调试灾难:无法重现
这是最狠的地方。
- body 执行顺序不可预测
- 调度策略因系统负载变化
- Debug / Release 行为不同
👉 你永远无法用断点推断逻辑顺序。
八、为什么这些问题在 UIKit 里“没这么严重”?
因为 UIKit:
- 你 手动 更新 UI
- 生命周期是线性的
- 副作用发生点固定
SwiftUI:
- Runtime 调度
- diff & 合并
- 并发预计算
👉 你失去了控制权,换来了确定性
👉 前提是:body 必须纯
九、症状速查表(真实项目版)
| 症状 | 高概率原因 |
|---|---|
| 网络请求莫名其妙重复 | body 里有副作用 |
| UI 偶尔卡死 | body 改 State |
| 内存慢慢涨但无泄漏 | body 注册监听 |
| 滚动掉帧 | body 做重计算 |
| ViewModel 状态丢失 | body new 对象 |
| Debug 正常 Release 崩 | body 并发不安全 |
最终一句话(工程级结论)
在 SwiftUI 中,
body里做副作用不会“立刻爆炸”,而是制造一颗“时间不定、地点不明、极难复现”的逻辑炸弹。
英文版
6-8. [Architecture Design] Why Must SwiftUI's body be a "Pure Function"?
This is the "Constitutional Rule" of SwiftUI.
The
bodymust be a pure function because SwiftUI treats it as a "reusable, repeatable, and concurrently invokable UI descriptor." Only pure functions ensure that diffing, scheduling, and concurrency remain safe and predictable.
In other words:
UI = f(State)
If f is "impure," the entire SwiftUI Runtime ceases to function correctly.
I. SwiftUI's "Implicit Assumptions" about body
SwiftUI is designed with several default assumptions:
bodycan be called any number of times.- The timing of the call is unpredictable.
- It may be executed on different threads (especially in future versions).
- The result depends solely on the input state.
Only when these assumptions hold true can SwiftUI perform:
- Diffing (Difference calculation)
- Coalescing (Merging updates)
- Skipping unnecessary refreshes
- Concurrent computation of the View tree
II. What is a "Pure Function" in the Context of SwiftUI?
In SwiftUI semantics, a "pure" body means:
✅ Allowed
- Reading State / Binding.
- Performing synchronous, deterministic calculations.
- Returning a View description.
❌ Forbidden
- Modifying State (directly or indirectly).
- Triggering Side Effects (Network calls, I/O, timers, notifications).
- Relying on external mutable global states.
- Relying on the number of calls or the calling order.
III. What Happens if body is Impure?
❌ Example: Changing State inside body
Swift
var body: some View {
count += 1 // ❌
return Text("(count)")
}
What happens?
body execution → count changes → State changes → triggers body again → Infinite Loop (often leading to a UI freeze).
❌ Example: Making a Request inside body
Swift
var body: some View {
fetchData() // ❌
return Text("Loading")
}
Consequences:
bodyis called multiple times.- The request is triggered repeatedly.
- Network storms / data inconsistency.
- Debugging becomes nearly impossible.
IV. Why Does SwiftUI Need This "Extreme Freedom"?
1. To Make the Diffing Algorithm Possible
The core of SwiftUI is:
Old View Description vs. New View Description → Minimal UI Update.
If body has side effects, the results before and after diffing become unpredictable, and the system cannot determine if a change "actually" occurred.
2. For Coalescing Updates
SwiftUI works like this:
State A changes + State B changes → Merge → Call body once.
If body is impure, the order of merging affects behavior, making the app unstable and dependent on scheduling strategies.
3. For Future Concurrency / Pre-computation
SwiftUI is already working on:
-
Calculating View trees on background threads.
-
Deferring or pre-calculating
body.If
bodyisn't pure, concurrent execution will cause data races, and Main Thread safety cannot be guaranteed.
V. Where Should Side Effects Go?
SwiftUI provides explicit "Side Effect Containers" :
| Scenario | API |
|---|---|
| First Appearance | .onAppear |
| State Change | .onChange(of:) |
| Async Tasks | .task {} |
| ViewModel Logic | ObservableObject |
| TCA Architecture | Effect / Reducer |
The commonality between these APIs is: They do not participate in the View diffing; they only participate in "Lifecycle Events."
VI. Why Does This Fit Perfectly with TCA / Redux?
Both share the same mathematical model:
Action → State → View
-
View = Pure Function
-
Reducer = Pure Function
-
Effect = Side Effect Container
This enables Time Travel, Snapshot Testing, and full UI Replays.
VII. Fundamental Differences from UIKit
| UIKit | SwiftUI |
|---|---|
| View is an Object | View is a Value |
| Manual UI Updates | Automatic Diffing |
| Lifecycle-Driven | State-Driven |
| Side Effects Everywhere | Centralized Side Effects |
UIKit can tolerate impurity because you control everything manually. SwiftUI cannot tolerate impurity because the Runtime handles everything for you.
Final Summary
SwiftUI's body must be a pure function—not as a "coding style preference," but as a prerequisite for the SwiftUI Runtime to exist, scale, and function concurrently.
If Side Effects are Performed in body, What Counter-Intuitive Bugs or Performance Issues Occur?
1. The Most Fatal: Invisible Infinite Loops
❌ Example: Modifying State in body
Swift
var body: some View {
if isLoggedIn == false {
isLoggedIn = checkLogin() // ❌
}
return Text("Hello")
}
What actually happens:
body executes → modifies State → SwiftUI marks View as dirty → calls body again → modifies State again...
Why it's "Counter-Intuitive":
You didn't write a while loop, and Xcode won't show an error, but CPU usage will spike, and the UI will freeze randomly.
2. Request Storms (From a Single Line of Code)
❌ Example: Network Request in body
Swift
var body: some View {
loadUser() // ❌
return Text(user.name)
}
Consequences:
body is called on: initial render, every state change, parent view refreshes, and environment changes (Dark Mode, Dynamic Type). Each call triggers a new request.
Symptoms: Network requests far exceed expectations, backend logs show anomalies, and data is occasionally overwritten.
3. "Ghost State" Bugs (The Most Terrifying)
❌ Example: Registering Observers or Timers in body
Swift
var body: some View {
NotificationCenter.default.addObserver(...) // ❌
return Text("Hello")
}
What actually happens: body is called N times, and the observer is registered N times. Even if you remove it once, the remaining observers persist.
Symptoms: Callbacks trigger multiple times, UI behavior becomes increasingly erratic, and memory slowly climbs. This isn't a memory leak; it's a "Logic Leak."
4. Performance Mines: Repeated Expensive Calculations
❌ Example: Heavy Computation
Swift
var body: some View {
let result = heavyCompute(data) // ❌
return Text(result)
}
The Trap: body is not "called only when needed." It might be called repeatedly during scrolling or parent state changes, leading to dropped frames and high CPU usage.
5. Concurrency Disasters
SwiftUI is already calculating View trees in parallel.
❌ Example: Accessing Shared Mutable Objects
Swift
var body: some View {
sharedCache.update() // ❌
return Text("OK")
}
Consequences: Data races, random state corruption, and crashes that occur in Release but not in Debug.
6. Identity Destruction: ViewModel Reconstruction Hell
❌ Example: Creating a new ViewModel in body
Swift
var body: some View {
let vm = ViewModel() // ❌
return ChildView(vm: vm)
}
Consequences: Every body call creates a new VM instance. Network requests restart, state is lost, and .onAppear behaves abnormally. It feels like SwiftUI is "randomly resetting" your page.Actually, it's you who destroyed identity within the body.
7. Debugging Disaster: Unreproducible Bugs
This is the most brutal part.
- The execution order of
bodyis unpredictable. - Scheduling strategies change based on system load.
- Behavior differs significantly between Debug and Release modes.
👉 You can never rely on breakpoints to infer logical sequence.
8. Why are these issues "less severe" in UIKit?
In UIKit:
- You manually update the UI.
- The lifecycle is linear.
- Points where side effects occur are fixed and explicit.
In SwiftUI:
- Execution is handled by Runtime scheduling.
- It involves diffing & coalescing (merging updates).
- It utilizes concurrent pre-computation.
👉 You give up manual control in exchange for declarative certainty. 👉 The prerequisite: body must be pure.
9. Symptoms Quick-Reference Table
| Symptom | Probable Cause |
|---|---|
| Mysterious duplicate network requests | Side effects in body |
| UI freezes randomly | body modifying State |
| Memory climbs without a "leak" | body registering listeners/observers |
| Dropped frames during scrolling | Heavy computation in body |
| ViewModel state lost | new object created in body |
| Works in Debug, crashes in Release | Concurrency-unsafe body |
Conclusion
In SwiftUI, performing side effects in body won't "explode" immediately; instead, it creates a "logic time bomb" that goes off at unpredictable times and locations, making it extremely difficult to reproduce.