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

使用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/相等判断问题:

  1. 修改DataSource类型定义:
typealias DataSource = UICollectionViewDiffableDataSource<UICollectionView.Section, UUID>
typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<UICollectionView.Section, UUID>
  1. 调整数据源配置逻辑:
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()
}
  1. 更新快照应用逻辑:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 18:15:51