一句话结论
在 SwiftUI 中,状态变化 = View 失效(invalidated),
状态粒度决定了“失效半径”;粒度越粗,每次状态变化牵连的 View 越多。
UIKit 是“你点哪我重画哪”,
SwiftUI 是“你动了谁的 State,我就重新计算谁的 body”。
一、为什么 SwiftUI 比 UIKit 更在乎“状态粒度”?
UIKit 的模型(命令式)
你决定:
- 更新哪个 label
- 刷新哪一行
- 是否 reload
你手动限制影响范围。
SwiftUI 的模型(声明式)
State 改变
→ 依赖该 State 的 View 全部失效
→ body 重新计算
SwiftUI 不问你想刷新多少,只按依赖关系算。
👉 所以:
状态“挂”在哪个 View 上,比状态本身更重要。
二、一个真实会导致卡顿的错误例子(重点)
场景:一个可滚动列表 + 搜索框
❌ 错误的状态设计(粒度过粗)
struct UserListView: View {
@State var searchText: String = ""
@State var users: [User] = [] // ❌ 整个列表状态
var body: some View {
VStack {
TextField("Search", text: $searchText)
List {
ForEach(filteredUsers) { user in
UserRow(user: user)
}
}
}
}
var filteredUsers: [User] {
users.filter { $0.name.contains(searchText) }
}
}
会发生什么?
每一次输入字符:
searchText 改变
→ UserListView body 重算
→ filteredUsers 重算(O(n))
→ List diff
→ 所有 Row body 重新计算
症状(真实项目表现)
- 输入时明显卡顿
- 列表越大越卡
- CPU 飙升
- Instruments 看不到明显“热点”
👉 因为你无意中把整个列表挂在了 searchText 的失效半径里。
三、为什么这是 SwiftUI 特有的痛点?
因为:
body是纯函数- SwiftUI 必须重新算整个依赖子树
- 它不知道你“其实只想动搜索框”
UIKit 里你可能只是:
reloadRows(at:)
SwiftUI 没这个入口。
四、正确的状态拆分方式(细粒度)
✅ 拆分 1:把搜索状态下沉
struct SearchBar: View {
@Binding var text: String
}
struct UserListView: View {
@State var searchText = ""
let users: [User]
var body: some View {
VStack {
SearchBar(text: $searchText)
UserList(users: users, searchText: searchText)
}
}
}
✅ 拆分 2:列表内部自己持有筛选结果
struct UserList: View {
let users: [User]
let searchText: String
var body: some View {
List {
ForEach(filteredUsers) { user in
UserRow(user: user)
}
}
}
var filteredUsers: [User] {
users.filter { $0.name.contains(searchText) }
}
}
现在:
searchText 改变
→ UserList 失效
→ SearchBar 不受影响
✅ 拆分 3(进阶):Row 级别状态独立
struct UserRow: View {
let user: User
@State var isExpanded = false
}
- 展开某一行
- 只重绘这一行
- 列表整体不受影响
五、另一个非常常见的卡顿陷阱(动画)
❌ 错误
struct CardList: View {
@State var isAnimating = false
var body: some View {
ForEach(cards) {
CardView(card: $0, animate: isAnimating)
}
}
}
- 一个卡片动画
- 触发全部卡片重绘
✅ 正确
struct CardView: View {
let card: Card
@State var isAnimating = false
}
👉 状态放在“最小变化单元”。
六、SwiftUI 的黄金法则(工程级)
State 应该放在“最小需要知道它的 View”上。
你可以把状态想成:
“爆炸半径”
- 粒度越粗 → 半径越大 → 性能越差
- 粒度越细 → 重绘越局部 → SwiftUI 才能快
七、状态粒度速查表
| 症状 | 可能原因 |
|---|---|
| 输入卡顿 | State 挂在父 View |
| 滚动掉帧 | Row 状态集中在 List |
| 动画不流畅 | 全局 State 驱动动画 |
| 小改动引发大刷新 | State 粒度过粗 |
最终一句话(压轴)
在 SwiftUI 中,状态不是“数据”,而是“失效范围声明”;
粒度选错,就等于告诉系统:‘每次都重算整个世界。’
英文版
6-10. [Architecture Design] Why is "State Granularity" More Critical in SwiftUI than in UIKit? (With Examples of Performance Lag and Over-Rendering)
One-sentence Conclusion
In SwiftUI, State Change = View Invalidation.
State granularity determines the "Invalidation Radius"; the coarser the granularity, the more Views are implicated in every state change.
UIKit follows the principle of: "I redraw exactly where you point."
SwiftUI follows the principle of: "I recalculate the body of whoever's State you touched."
I. Why does SwiftUI care more about "State Granularity" than UIKit?
UIKit Model (Imperative)
You decide:
- Which label to update
- Which row to refresh
- Whether to reload the entire component
You manually limit the scope of impact.
SwiftUI Model (Declarative)
State changes
→ All Views dependent on that State are invalidated
→ body is recalculated
SwiftUI doesn't ask how much you want to refresh; it calculates based on dependencies.
👉 Therefore:
Where the state is "attached" (which View holds it) is more important than the state itself.
II. A Real-World Performance Trap (Critical Example)
Scenario: A Scrollable List + Search Bar
❌ Poor State Design (Coarse Granularity)
Swift
struct UserListView: View {
@State var searchText: String = ""
@State var users: [User] = [] // ❌ Entire list state
var body: some View {
VStack {
TextField("Search", text: $searchText)
List {
ForEach(filteredUsers) { user in
UserRow(user: user)
}
}
}
}
var filteredUsers: [User] {
users.filter { $0.name.contains(searchText) }
}
}
What happens?
Every time a character is typed:
searchText changes
→ UserListView body recalculates
→ filteredUsers recalculates (O(n))
→ List performs a diff
→ All UserRow bodies are recalculated
Symptoms (Real-world project behavior)
- Noticeable lag during typing.
- The larger the list, the worse the lag.
- CPU usage spikes.
- Instruments (Profiling) shows no obvious "hotspots."
👉 Because you accidentally placed the entire list inside the invalidation radius of searchText.
III. Why is this a SwiftUI-specific pain point?
Because:
bodyis a pure function.- SwiftUI must recalculate the entire dependent subtree.
- It doesn't know that you "actually only wanted to move the search bar."
In UIKit, you would simply call:
Swift
reloadRows(at:)
SwiftUI has no such entry point.
IV. Correct State Splitting (Fine Granularity)
✅ Split 1: Push the search state down
Swift
struct SearchBar: View {
@Binding var text: String
}
struct UserListView: View {
@State var searchText = ""
let users: [User]
var body: some View {
VStack {
SearchBar(text: $searchText)
UserList(users: users, searchText: searchText)
}
}
}
✅ Split 2: The List holds its own filtering results
Swift
struct UserList: View {
let users: [User]
let searchText: String
var body: some View {
List {
ForEach(filteredUsers) { user in
UserRow(user: user)
}
}
}
var filteredUsers: [User] {
users.filter { $0.name.contains(searchText) }
}
}
Now:
searchText changes
→ UserList is invalidated
→ SearchBar is unaffected
✅ Split 3 (Advanced): Independent Row-level State
Swift
struct UserRow: View {
let user: User
@State var isExpanded = false
}
- Expanding one row only redraws that specific row.
- The overall list remains unaffected.
V. Another Common Performance Trap (Animations)
❌ Incorrect
Swift
struct CardList: View {
@State var isAnimating = false
var body: some View {
ForEach(cards) {
CardView(card: $0, animate: isAnimating)
}
}
}
- A single card's animation triggers a redraw for all cards.
✅ Correct
Swift
struct CardView: View {
let card: Card
@State var isAnimating = false
}
👉 Keep the state within the "Smallest Unit of Change."
VI. The Golden Rule of SwiftUI (Engineering Level)
State should reside in the "smallest possible View that needs to know about it."
Think of state as the:
"Blast Radius"
- Coarser granularity → Larger radius → Worse performance.
- Finer granularity → Targeted redrawing → SwiftUI can stay fast.
VII. State Granularity Quick-Check Table
| Symptom | Probable Cause |
|---|---|
| Typing Lag | State is attached to a Parent View |
| Dropped Frames on Scroll | Row state is centralized in the List |
| Stuttering Animations | Global State driving the animation |
| Small change causes massive refresh | State granularity is too coarse |
Final Takeaway
In SwiftUI, State is not just "data"; it is a "Declaration of Invalidation Scope." If you choose the wrong granularity, you are telling the system: "Recalculate the entire world every single time."