You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 20:45:24