使用Combine+@resultBuilder构建UIKit动态UICollectionView列表的两个问题
问题1:点击两次才更新的原因及解决方案
原因
@Published 发布者的回调触发时机是属性变更前(willSet语义)。你在sink回调中调用snapshot()生成快照时,闭包捕获的self.testItems还没有被赋值为新值,拿到的仍然是上一次的旧值,生成的快照自然也是旧的。新值要等sink回调执行完成后才会写入testItems,所以永远慢一拍,需要第二次点击才能把上一次的值渲染出来。
解决方案
更推荐的方案是调整闭包设计,把sink收到的最新值直接传给快照构建闭包,避免闭包直接捕获可能过时的self.testItems:
- 修改
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) }
- 调整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
相关产品推荐
相关产品推荐

