SwiftUI长列表更新单元格时出现明显卡顿问题求助
SwiftUI长列表更新卡顿问题排查与优化
我开发的应用中,长列表通过LazyVStack展示,数据存储在字典和索引列表中,单元格显示时触发服务器数据拉取,拉取完成后更新对应单元格。本以为现代iPhone不会出现卡顿,但实际动画流畅度明显下降,真实应用的卡顿情况比示例更严重,核心代码如下:
struct ContentView: View { var body: some View { TestView() .environmentObject(globalState) .task { (0...10000).forEach { _ in globalState.newItem() } updateLater() } } func updateLater() { DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) { update() } } func update() { DispatchQueue.main.async { let id = globalState.itemIds[3] var item = globalState.itemDict[id]! item.newColor() item.text = "changed" globalState.itemDict[id] = item } updateLater() } } extension State { func newItem() { let item = Item() itemDict[item.id] = item itemIds.append(item.id) } } let globalState = State() struct TestView: View { @EnvironmentObject var state: State var items: [Item] { state.itemList.filter { !$0.text.isEmpty } } var body: some View { ScrollView { LazyVStack { ForEach(items, id: \.id) { item in row(item) } } } } func row(_ item: Item) -> some View { Button { } label: { Text(item.text) .frame(maxWidth: .infinity) .padding() .background(item.color.opacity(0.2)) } } } class State: ObservableObject { @Published var itemIds: [UUID] = [] @Published var itemDict: [UUID: Item] = [:] var itemList: [Item] { itemIds.compactMap { itemDict[$0] } } } func randomColor() -> Color { Color(hue: .random(in: 0...1), saturation: 0.9, brightness: 0.5) } struct Item { var id = UUID() var text = "Hello" var color = randomColor() mutating func newColor() { color = randomColor() } }
卡顿核心原因
- ObservableObject发布范围过大:每次更新
itemDict时,整个@Published var itemDict都会被标记为变化,导致依赖它的TestView和整个LazyVStack重新计算,而非仅更新变化的单元格。 - 重复计算开销:
TestView中的items属性每次都会基于state.itemList重新过滤,10000条数据的重复过滤会带来额外性能损耗。 - 值类型更新触发全量通知:修改值类型
Item时,需要取出、修改、再赋值回字典,这个操作会触发整个字典的发布通知,而非单个Item的局部更新。
优化方案
1. 将Item改为ObservableObject实现精准更新
把Item从值类型改为引用类型的ObservableObject,这样修改单个Item的属性时,只会通知对应的单元格刷新,不会触发整个列表的重新计算:
class Item: ObservableObject, Identifiable { let id = UUID() @Published var text = "Hello" @Published var color = randomColor() func newColor() { color = randomColor() } }
2. 调整State的存储与发布逻辑
不再发布整个字典,直接维护Item数组(若需快速查找可保留私有字典,不对外发布),同时避免重复计算过滤后的列表:
class State: ObservableObject { @Published var items: [Item] = [] private var itemDict: [UUID: Item] = [:] func newItem() { let item = Item() items.append(item) itemDict[item.id] = item } func updateItem(withId id: UUID) { guard let item = itemDict[id] else { return } item.newColor() item.text = "changed" } }
3. 简化视图层级与计算逻辑
将单元格抽为独立视图,用@ObservedObject监听单个Item的变化,避免每次列表刷新都重新创建所有单元格:
struct TestView: View { @EnvironmentObject var state: State var body: some View { ScrollView { LazyVStack { ForEach(state.items) { item in ItemRow(item: item) } } } } } struct ItemRow: View { @ObservedObject var item: Item var body: some View { Button {} label: { Text(item.text) .frame(maxWidth: .infinity) .padding() .background(item.color.opacity(0.2)) } } }
4. 优化更新触发逻辑
直接通过State的方法修改Item,避免字典的重复赋值操作:
func update() { DispatchQueue.main.async { let id = globalState.items[3].id globalState.updateItem(withId: id) } updateLater() }
额外优化建议
- 替换全局
globalState为@StateObject,确保状态的生命周期与视图绑定,避免不必要的内存占用和通知。 - 服务器拉取完成后,直接更新对应的Item实例,不要触发整个列表的数据源刷新。
- 超大规模列表采用分页加载,避免一次性加载10000条数据到内存。
内容的提问来源于stack exchange,提问作者yspreen
相关产品推荐
相关产品推荐

