UICollectionView Diffable Data Source如何保留跨区移动动画并更新header内容
问题原因
Diffable Data Source 依赖 Section 标识符的 Hashable/Equatable 协议实现判断两个 section 是否为同一个。你给 checked case 增加了关联数量后,Swift 自动合成的协议实现会把关联的 Int 值纳入判断,导致数量变化前后的 .checked(旧数量) 和 .checked(新数量) 被识别为两个完全不同的 section。Diffable 会直接删除旧 section 插入新 section,自然无法触发跨 section 的 item 移动动画。
解决方案
推荐两种可行方案,优先推荐第一种更符合 Diffable 设计规范的实现:
方案1:展示数据与 Section 标识符分离(推荐)
核心原则:Diffable 的 Section/Item 标识符只负责唯一标识,不要存放视图展示用的动态数据。
- 把 Section 定义改回无关联值的版本:
enum Section: Hashable { case unchecked case checked }
- 恢复原来的
applySnapshot逻辑,不需要给 Section 传数量。 - header 展示时直接读取当前已勾选列表的数量:
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in guard kind == UICollectionView.elementKindSectionHeader, let checklistHeader = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: "header", for: indexPath ) as? ChecklistHeader else { return nil } let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section] switch section { case .unchecked: checklistHeader.label.text = "Header with unchecked item" case .checked: // 直接读当前已勾选数组的数量,不需要存在Section里 checklistHeader.label.text = "Header with checked item \(self.checkedChecklists.count)" } checklistHeader.delegate = self return checklistHeader }
这种方案不需要修改协议实现,逻辑更简洁,后续维护成本更低。
方案2:自定义 Section 的 Hashable/Equatable 实现
如果一定要把数量存在 Section 的关联值里,可以手动重写协议方法,让关联的数量不参与相等判断和哈希计算:
enum Section: Hashable { case unchecked case checked(Int) static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.unchecked, .unchecked): return true case (.checked, .checked): return true default: return false } } func hash(into hasher: inout Hasher) { switch self { case .unchecked: hasher.combine(0) case .checked: hasher.combine(1) } } }
修改后无论 .checked 携带的数量是多少,都会被识别为同一个 section,既可以保留移动动画,也可以通过关联值传递数量给 header 展示。
内容的提问来源于stack exchange,提问作者Cheok Yan Cheng
相关产品推荐
相关产品推荐

