如何用KVO更新Collection View?单元格进度不更新问题排查
CollectionView单元格进度标签不更新问题排查
我希望在Collection View单元格的文本标签中显示进度,使用以下两种方案实现时均无效:观察者代码能打印进度但单元格不显示,通知方案点击后集合视图消失,请问原因是什么?
KVO方案(无效)
代码如下:
var nameObservation: NSKeyValueObservation? @objc dynamic var progress = 0.0 func createDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in switch self.sections[indexPath.section].identifier { case "carouselCell": let cell = self.configure(CarouselCell.self, with: item, for: indexPath) self.nameObservation = self.observe(\.progress, options: .new) { vc, change in cell.title.text = "\(self.progress)" } return cell default: return self.configure(CarouselCell.self, with: item, for: indexPath) } } } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { _ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in guard self.progress <= 1.0 else { timer.invalidate() self.progress = 0.0 return } } }
问题原因:
- Timer未更新进度值:
didSelectItemAt中的Timer仅做了进度上限判断,但没有执行progress += 增量的操作,progress始终为初始值0.0,观察者自然不会触发更新。 - 观察者被覆盖:每次创建
carouselCell时都会重新赋值nameObservation,之前的观察者会被释放,只有最后创建的单元格能绑定到观察者。 - 单元格复用风险:直接修改cell的文本标签,当单元格被复用(滚出屏幕再返回)时,观察者绑定的cell已经不是原来的实例,更新会失效或错乱。
通知方案(无效)
代码如下:
var nameObservation: NSKeyValueObservation? @objc func createDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in switch self.sections[indexPath.section].identifier { case "carouselCell": let cell = self.configure(CarouselCell.self, with: item, for: indexPath) cell.title.text = "\(self.progressA)" print(self.progressA) return cell default: return self.configure(CarouselCell.self, with: item, for: indexPath) } } } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { NotificationCenter.default.addObserver( self, selector: #selector(self.createDataSource), name: Notification.Name(rawValue: "sound-CarouselController"), object: nil) _ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in guard self.progressA <= 1.0 else { timer.invalidate() self.progressA = 0.0 return } self.progressA += 0.01 NotificationCenter.default.post(name: Notification.Name(rawValue: "sound-CarouselController"), object: nil) } }
问题原因:
- 重复添加观察者:每次点击单元格都会添加新的通知观察者,导致进度变化时
createDataSource被多次调用,数据源频繁重置。 - 错误的数据源更新方式:通知触发时重新创建
dataSource,而非更新数据源快照(Snapshot),这会导致CollectionView重新初始化,表现为“视图消失”;同时新创建的dataSource没有基于最新的progressA生成正确的单元格内容。 - 进度与单元格未绑定:
progressA是全局属性,所有单元格共用同一个值,无法实现单个单元格独立显示进度。
正确解决方案
核心思路:将进度绑定到单元格对应的Item模型,利用DiffableDataSource的快照更新机制刷新单元格
- 修改Item模型:给Item添加可观察的进度属性
class Item: Hashable { // 原有属性 var id: String @objc dynamic var progress: Double = 0.0 // 实现Hashable协议(根据实际情况调整) static func == (lhs: Item, rhs: Item) -> Bool { return lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } }
- 正确配置DataSource与KVO:每个单元格观察对应Item的进度,避免复用问题
func createDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { [weak self] collectionView, indexPath, item in guard let self = self else { return nil } switch self.sections[indexPath.section].identifier { case "carouselCell": let cell = self.configure(CarouselCell.self, with: item, for: indexPath) // 观察当前Item的progress,用cell的关联对象存储观察者,避免被释放 let observation = item.observe(\.progress, options: .new) { [weak cell] _, _ in DispatchQueue.main.async { cell?.title.text = "\(item.progress)" } } objc_setAssociatedObject(cell, &ObservationKey, observation, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return cell default: return self.configure(CarouselCell.self, with: item, for: indexPath) } } } // 定义关联对象的Key private var ObservationKey = "ObservationKey"
- 更新进度并刷新快照:点击单元格时,更新对应Item的进度,然后apply快照刷新单元格
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let item = dataSource.itemIdentifier(for: indexPath) else { return } _ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { [weak self] timer in guard let self = self else { return } guard item.progress <= 1.0 else { timer.invalidate() item.progress = 0.0 // 更新快照 var snapshot = self.dataSource.snapshot() snapshot.reloadItems([item]) self.dataSource.apply(snapshot) return } item.progress += 0.01 // 更新快照,刷新对应单元格 var snapshot = self.dataSource.snapshot() snapshot.reloadItems([item]) self.dataSource.apply(snapshot) } }
补充说明:
- 使用
weak self和weak cell避免循环引用; - 用关联对象存储每个cell的观察者,防止观察者被提前释放;
- 必须在主线程更新UI,所以观察者中用
DispatchQueue.main.async; - 利用DiffableDataSource的快照更新,既保证单元格刷新的正确性,也避免了复用问题。
内容的提问来源于stack exchange,提问作者User
相关产品推荐
相关产品推荐

