SwiftUI 中的高级列表与分页
发布: (2025年12月19日 GMT+8 08:07)
5 min read
原文: Dev.to
Source: Dev.to
列表看起来很简单——直到你尝试构建真实的动态。
然后你会遇到以下问题:
- 无限滚动卡顿
- 行重复
- 分页触发过于频繁
- 滚动位置跳动
- 重行导致性能下降
- 异步竞争
- 到处都是加载状态
- 离线 + 分页冲突
本文展示了 生产环境的 SwiftUI 应用 如何处理列表和分页——干净、快速、可预测且可扩展。
🧠 核心原则
- 增量加载数据
- 永不阻塞滚动
- 保持身份稳定
- 避免重复请求
- 优雅地处理错误
- 支持离线数据
- 在大数据集下保持流畅
🧱 1. 正确的列表架构
Separate concerns:
View
↓
ViewModel (pagination state)
↓
Service (fetch pages)
ViewModel 拥有分页逻辑——而不是视图。
📦 2. 分页状态模型
@Observable
class FeedViewModel {
var items: [Post] = []
var isLoading = false
var hasMore = true
var page = 0
}
这使得分页:
- 可预测的
- 易于调试的
- 可测试的
🔄 3. 安全获取页面
@MainActor
func loadNextPage() async {
guard !isLoading, hasMore else { return }
isLoading = true
defer { isLoading = false }
do {
let response = try await service.fetch(page: page)
items.append(contentsOf: response.items)
hasMore = response.hasMore
page += 1
} catch {
errors.present(map(error))
}
}
关键规则
- 防止重复调用
- 在主 actor 上更新状态
- 追加,永不替换
- 明确跟踪
hasMore
📜 4. 从视图触发分页(安全)
List {
ForEach(items) { item in
RowView(item: item)
.onAppear {
if item == items.last {
Task { await viewModel.loadNextPage() }
}
}
}
if viewModel.isLoading {
ProgressView()
.frame(maxWidth: .infinity)
}
}
这段代码:
- 支持动态行高
- 在旋转时仍能正常工作
- 避免
GeometryReader陷阱
🆔 5. 身份不可协商
错误的身份会导致列表性能下降。
ForEach(items, id: \.id) { ... }
规则
- ID 必须保持稳定
- 切勿在行内生成
UUID - 切勿使用数组索引
- 切勿修改 ID
错误的身份会导致:
- 行重复
- 动画卡顿
- 滚动跳动
- 行之间的状态泄漏
⚠️ 6. 避免在行内部进行繁重工作
绝不这样做:
RowView(item: item)
.task {
await expensiveWork()
}
而是
- 在 ViewModel 中预先计算
- 缓存结果
- 将轻量级数据传入行
行应该是:
- 轻量的
- 纯粹的
- 渲染快速
🧊 7. 骨架屏 & 占位行
if items.isEmpty && isLoading {
ForEach(0..<5) { _ in
SkeletonRow()
}
}
这:
- 保持布局
- 感觉更快
- 避免 UI 跳动
📶 8. 离线 + 分页
当离线时:
- 加载缓存页面
- 禁用分页
- 保持滚动流畅
示例:
if !network.isOnline {
hasMore = false
}
恢复在线后自动重试。
🔁 9. 下拉刷新而不重置所有内容
.refreshable {
page = 0
hasMore = true
items.removeAll()
await loadNextPage()
}
避免:
- 重建 ViewModel
- 重置身份
- 不必要地清除滚动状态
⚖️ 10. Performance Rules for Large Lists
- ✅ 使用
List处理大型数据集 - ✅ 在
ScrollView中使用LazyVStack进行自定义布局 - ✅ 避免在行中使用
GeometryReader - ✅ 保持行结构浅层
- ✅ 积极缓存图像
- ✅ 避免对每行进行环境更新
- ✅ 避免嵌套列表
SwiftUI 列表在正确使用时性能极快。
🧪 11. 测试分页逻辑
因为逻辑位于 ViewModel 中:
func test_pagination_appends() async {
let vm = FeedViewModel(service: MockService())
await vm.loadNextPage()
await vm.loadNextPage()
XCTAssertEqual(vm.items.count, 40)
}
不需要 UI,也不需要滚动模拟——纯逻辑测试。
🧠 心智模型速查表
自问:
- 分页状态的所有者是谁?
- 会出现重复请求吗?
- 身份是否稳定?
- 行是否轻量?
- 离线会破坏它吗?
如果所有答案都清晰 → 你的列表将具备可扩展性。
🚀 最后思考
高级列表并不是关于巧妙的技巧。它们涉及:
- 明确的所有权
- 稳定的标识
- 可预测的分页
- 最小的行工作量
- 干净的异步处理
做好这些,你的 SwiftUI 列表将感觉原生、快速且坚如磐石——即使拥有数万行。