一句话结论
在 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 中,状态不是“数据”,而是“失效范围声明”;
粒度选错,就等于告诉系统:‘每次都重算整个世界。’