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

如何用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
        }
    }
}

问题原因:

  1. Timer未更新进度值:didSelectItemAt中的Timer仅做了进度上限判断,但没有执行progress += 增量的操作,progress始终为初始值0.0,观察者自然不会触发更新。
  2. 观察者被覆盖:每次创建carouselCell时都会重新赋值nameObservation,之前的观察者会被释放,只有最后创建的单元格能绑定到观察者。
  3. 单元格复用风险:直接修改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)
    }
}

问题原因:

  1. 重复添加观察者:每次点击单元格都会添加新的通知观察者,导致进度变化时createDataSource被多次调用,数据源频繁重置。
  2. 错误的数据源更新方式:通知触发时重新创建dataSource,而非更新数据源快照(Snapshot),这会导致CollectionView重新初始化,表现为“视图消失”;同时新创建的dataSource没有基于最新的progressA生成正确的单元格内容。
  3. 进度与单元格未绑定:progressA是全局属性,所有单元格共用同一个值,无法实现单个单元格独立显示进度。

正确解决方案

核心思路:将进度绑定到单元格对应的Item模型,利用DiffableDataSource的快照更新机制刷新单元格

  1. 修改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)
    }
}
  1. 正确配置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"
  1. 更新进度并刷新快照:点击单元格时,更新对应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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 16:44:54