Swift DiffableDataSource在Firebase .observe()更新值时崩溃如何解决?
我正在开发一款文章阅读类应用,文章页内点击作者头像可跳转至作者个人主页。
作者主页内包含Follow按钮,同时展示作者相关统计数据(如发布文章数量、粉丝数等),界面样式参考下图:
页面首次加载时运行正常,但用户点击Follow按钮后应用立即崩溃,报错信息如下:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Section identifier count does not match data source count. This is most likely due to a hashing issue with the identifiers.'
复现步骤
- 用户进入作者个人主页
- 页面展示Follow按钮以及作者粉丝数等统计数据,假设当前作者粉丝数为10
- 用户点击Follow按钮
- 后台逻辑通过
.observeSingleEvent()获取作者数据,调用setValue()将当前用户ID添加到followers字符串数组:followers: ["asdm132123", "asdadsa12931", "!123123jsdfjsf"]中 - 随后通过Firebase的
.observe()方法监听数据更新,将按钮状态从Follow改为Following,同时将粉丝数从10更新为11,此时触发崩溃
相关代码
枚举定义
enum AuthorProfileSection { case details case stats case articles }
数据源创建代码
func configureDataSource() -> UICollectionViewDiffableDataSource<AuthorProfileSection, AnyHashable> { let dataSource = UICollectionViewDiffableDataSource<AuthorProfileSection, AnyHashable>(collectionView: collectionView) { (collectionView, indexPath, object) -> UICollectionViewCell? in if let object = object as? Author { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AuthorDetailsCell.reuseIdentifier, for: indexPath) as! AuthorDetailsCell cell.configure(with: object) return cell } if let object = object as? AuthorStatsForAuthorProfile { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AuthorStatCell.reuseIdentifier, for: indexPath) as! AuthorStatCell cell.configure(with: object) return cell } if let object = object as? Article { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AuthorArticleCell.reuseIdentifier, for: indexPath) as! AuthorArticleCell cell.configure(with: object) return cell } return nil } return dataSource }
快照更新代码
fileprivate func updateSnapshot(animated: Bool = false) { guard let authorUID = authorUID else { return } Firebase.Database.database().fetchSelectedAuthorProfileData( authorUID: authorUID, fromArticleUID: self.articleUID) { authorProfileSection in var snapshot = self.dataSource.snapshot() // sections snapshot.appendSections([.details, .stats, .articles]) snapshot.appendItems([authorProfileSection.details], toSection: .details) snapshot.appendItems(authorProfileSection.stats ?? [], toSection: .stats) snapshot.appendItems(authorProfileSection.articles ?? [], toSection: .articles) self.dataSource.apply(snapshot, animatingDifferences: animated) } }
Cell代码
class AuthorStatCell: UICollectionViewCell, SelfConfiguringCell { typealias someType = AuthorStatsForAuthorProfile static var reuseIdentifier = "AuthorStatCell" override init(frame: CGRect) { super.init(frame: frame) buildUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(with data: AuthorStatsForAuthorProfile) { print(data) } fileprivate func buildUI() { backgroundColor = UIColor(red: 0.20, green: 0.83, blue: 0.60, alpha: 1.00) layer.cornerRadius = 15 } }
界面效果如下,红色区域为点击关注按钮后会变更的内容:
核心原因
快照更新逻辑存在错误:你每次更新快照时,都是基于当前数据源已有的快照(首次加载后已经包含.details、.stats、.articles三个section)直接调用appendSections重复添加相同的section。首次加载时快照为空,添加操作无异常;点击关注后触发数据回调,第二次更新快照时重复添加已存在的section标识符,导致快照内部的section计数和预期不符,触发系统的一致性校验异常,就是你看到的崩溃报错。
额外风险点
你使用AnyHashable作为DiffableDataSource的item类型,如果AuthorStatsForAuthorProfile、Author、Article等数据模型没有正确实现Hashable协议(例如没有将数据的唯一ID作为哈希计算的核心依据,而是用全量属性计算哈希),当数据属性更新(比如粉丝数变化)后,新实例和旧实例的哈希值不匹配,会导致快照无法正确识别为同一条数据的更新,反而触发新增/删除逻辑异常,也会引发同类崩溃。
修复方案
- 每次更新快照时创建全新的快照实例,不要复用当前数据源的已有快照,避免重复添加已存在的section和item,将快照更新代码中的
var snapshot = self.dataSource.snapshot()修改为var snapshot = NSDiffableDataSourceSnapshot<AuthorProfileSection, AnyHashable>()即可。 - 建议不要使用
AnyHashable作为item类型,给所有item模型定义统一的协议并显式实现Hashable,保证每个数据项的唯一标识符作为哈希计算的核心,属性变更不会影响同一条数据的哈希匹配。 - 如果需要复用已有快照,要先调用
deleteAllItems()、deleteSections()等方法清空旧内容后再添加新内容。
内容的提问来源于stack exchange,提问作者christostsang

