使用UICollectionView DiffableDataSource时出现重复标识符致命错误
解决UICollectionView DiffableDataSource重复标识符致命错误
我在使用UICollectionView的DiffableDataSource时,持续遇到以下致命错误:
Fatal: supplied item identifiers are not unique. Duplicate identifiers
实现代码
FeedsCollectionView 代码
extension UICollectionView { enum Section { case main } } class FeedsCollectionView: View { var collectionView: UICollectionView! = nil let layout:UICollectionViewFlowLayout = UICollectionViewFlowLayout.init() typealias DataSource = UICollectionViewDiffableDataSource<UICollectionView.Section, Scraper> typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<UICollectionView.Section, Scraper> private var dataSource : DataSource! private var snapshot: DataSourceSnapshot! public lazy var data: [Scraper] = [] { didSet { applySnapshot() } } private func configureDataSource() { dataSource = DataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, data) in if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "feeds", for: indexPath) as? FeedsCollectionViewCell { cell.configure(with: data) cell.eventHandler = { [weak self] events in switch events { case .reload: self?.applySnapshot() } } return cell } return UICollectionViewCell() }) applySnapshot() } public func applySnapshot() { snapshot = DataSourceSnapshot() snapshot.appendSections([UICollectionView.Section.main]) snapshot.appendItems(data) dataSource.apply(snapshot, animatingDifferences: true) } private func createLayout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { section, layoutEnvironment in var config = UICollectionLayoutListConfiguration(appearance: .sidebar) config.headerMode = section == 0 ? .none : .firstItemInSection return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } private func configureHierarchy() { collectionView = UICollectionView(frame: bounds, collectionViewLayout: createLayout()) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.register(FeedsCollectionViewCell.self, forCellWithReuseIdentifier: "feeds") addSubview(collectionView) } override func setupUI() { configureHierarchy() configureDataSource() } override func setupLayout() { LayoutConstraint.pin(to: .bounds, collectionView).activate() } }
Scraper 类部分代码
public class Scraper: Hashable { let id = UUID() public func hash(into hasher: inout Hasher) { hasher.combine(id) } public static func == (lhs: Scraper, rhs: Scraper) -> Bool { lhs.id == rhs.id } }
Scraper类规模较大,无法改为结构体,请问该如何解决这个错误?
解决方案
方案一:排查并修复数据源重复问题
虽然Scraper通过UUID实现了Hashable,但错误说明data数组中存在标识重复的元素。可以先添加代码定位问题:
public func applySnapshot() { // 检查数组中是否存在重复ID的元素 let idGroups = Dictionary(grouping: data, by: { $0.id }) let duplicateIds = idGroups.filter { $0.value.count > 1 }.keys if !duplicateIds.isEmpty { print("发现重复ID: \(duplicateIds),对应元素数量: \(idGroups.filter { $0.value.count > 1 }.values.map { $0.count })") } snapshot = DataSourceSnapshot() snapshot.appendSections([UICollectionView.Section.main]) snapshot.appendItems(data) dataSource.apply(snapshot, animatingDifferences: true) }
通过日志找到重复ID后,检查data数组的更新逻辑:
- 是否存在同一个Scraper实例被多次添加到数组的情况
- 是否在多线程环境下修改
data导致的异常重复 - 是否有其他代码意外修改了Scraper的
id属性(尽管它被声明为let,但需排查是否有反射/KVC等非常规修改操作)
方案二:改用UUID作为DiffableDataSource的标识类型
将DiffableDataSource的item标识从引用类型Scraper改为值类型UUID,彻底避免引用类型可能带来的hash/相等判断问题:
- 修改DataSource类型定义:
typealias DataSource = UICollectionViewDiffableDataSource<UICollectionView.Section, UUID> typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<UICollectionView.Section, UUID>
- 调整数据源配置逻辑:
private func configureDataSource() { dataSource = DataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, itemId) in // 通过UUID找到对应的Scraper实例 guard let scraper = self.data.first(where: { $0.id == itemId }) else { return UICollectionViewCell() } if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "feeds", for: indexPath) as? FeedsCollectionViewCell { cell.configure(with: scraper) cell.eventHandler = { [weak self] events in switch events { case .reload: self?.applySnapshot() } } return cell } return UICollectionViewCell() }) applySnapshot() }
- 更新快照应用逻辑:
public func applySnapshot() { snapshot = DataSourceSnapshot() snapshot.appendSections([UICollectionView.Section.main]) // 传入Scraper的UUID数组作为标识 snapshot.appendItems(data.map { $0.id }) dataSource.apply(snapshot, animatingDifferences: true) }
这种方式将数据实体与DiffableDataSource的标识分离,利用值类型UUID的唯一性保证标识不重复,同时无需修改Scraper类的结构。
内容的提问来源于stack exchange,提问作者martin wotterman
相关产品推荐
相关产品推荐

