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

使用Combine+@resultBuilder构建UIKit动态UICollectionView列表的两个问题

问题1:点击两次才更新的原因及解决方案

原因

@Published 发布者的回调触发时机是属性变更前(willSet语义)。你在sink回调中调用snapshot()生成快照时,闭包捕获的self.testItems还没有被赋值为新值,拿到的仍然是上一次的旧值,生成的快照自然也是旧的。新值要等sink回调执行完成后才会写入testItems,所以永远慢一拍,需要第二次点击才能把上一次的值渲染出来。

解决方案

更推荐的方案是调整闭包设计,把sink收到的最新值直接传给快照构建闭包,避免闭包直接捕获可能过时的self.testItems:

  1. 修改List的初始化方法:
init(_ items: Published<[String]>.Publisher, style: UICollectionLayoutListConfiguration.Appearance = .insetGrouped, @SnapshotBuilder snapshot: @escaping ([String]) -> NSDiffableDataSourceSectionSnapshot<ListItem>) {
    super.init(frame: .zero, collectionViewLayout: List.createLayout(style))
    configureDataSource()
    // @Published订阅时会立即发送当前值,无需单独处理首次渲染
    items
        .sink { [weak self] newValue in
            guard let self = self else { return }
            // 直接用回调返回的最新值生成快照,没有时序依赖
            let newSnapshot = snapshot(newValue)
            self.data.apply(newSnapshot, to: .main, animatingDifferences: true)
        }
        .store(in: &cancellables)
}
  1. 调整ViewController的调用代码:
collectionView = List($testItems) { items in
    for item in items {
        ListItem(item)
    }
}

如果不想修改现有接口,也可以临时用异步派发的方式等属性赋值完成后再生成快照:

items
    .sink { [weak self] _ in
        DispatchQueue.main.async {
            let newSnapshot = snapshot()
            self?.data.apply(newSnapshot, to: .main, animatingDifferences: true)
        }
    }
    .store(in: &cancellables)

问题2:Snapshot创建逻辑优化

首先不需要过度担心每次创建新Snapshot的性能问题:NSDiffableDataSource在apply时会自动对新旧快照做差异化计算,只会更新变化的部分,全量创建快照的开销在普通列表场景下完全可以忽略。

如果确实要做增量优化,可以保留当前快照实例做增量修改,不需要每次全量重建:

final class List: UICollectionView {
    // 保存当前的section快照
    private var currentSnapshot: NSDiffableDataSourceSectionSnapshot<ListItem> = .init()
    // ... 其他原有代码
    init(_ items: Published<[String]>.Publisher, style: UICollectionLayoutListConfiguration.Appearance = .insetGrouped, @SnapshotBuilder snapshot: @escaping ([String]) -> NSDiffableDataSourceSectionSnapshot<ListItem>) {
        super.init(frame: .zero, collectionViewLayout: List.createLayout(style))
        configureDataSource()
        items
            .sink { [weak self] newValue in
                guard let self = self else { return }
                // 仅示例增量添加逻辑,删除、移动逻辑可以按需扩展
                let existingTitles = self.currentSnapshot.items.map(\.title)
                let newItems = newValue.filter { !existingTitles.contains($0) }.map { ListItem($0) }
                self.currentSnapshot.append(newItems)
                self.data.apply(self.currentSnapshot, to: .main, animatingDifferences: true)
            }
            .store(in: &cancellables)
    }
}

内容的提问来源于stack exchange,提问作者Niklas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 07:15:04