如何重构双模型DiffableDataSource以支持UICollectionView原生更新?
重构方案:让DiffableDataSource支持原生更新
核心问题拆解
你当前的困境本质是两个问题:
- 用
AnyHashable作为Item标识符,DiffableDataSource无法精准区分User和Chat的身份,也无法感知Chat关联数据的变化; - ChatCell直接依赖外部
tagsManager、statusManager,导致UI数据分散在多个地方,DiffableDataSource无法通过快照对比识别需要更新的单元格。
下面是具体的重构步骤,每一步都配代码示例:
1. 替换AnyHashable为明确的Item枚举
先定义一个枚举作为DiffableDataSource的Item类型,明确区分User和Chat的身份,同时保证每个Item的唯一性:
enum CollectionItem: Hashable { case user(User) case chat(ChatViewModel) // 基于唯一ID做哈希和相等判断,确保Diffable能识别同一个Item func hash(into hasher: inout Hasher) { switch self { case .user(let user): hasher.combine(user.id) case .chat(let vm): hasher.combine(vm.chat.id) } } static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.user(let lhsUser), .user(let rhsUser)): return lhsUser.id == rhsUser.id case (.chat(let lhsVM), .chat(let rhsVM)): return lhsVM.chat.id == rhsVM.chat.id default: return false } } }
2. 创建ChatViewModel整合所有UI依赖数据
把ChatModel、tagsManager的标签、statusManager的状态全部整合到一个ViewModel里,让Cell只依赖这个ViewModel,同时实现Hashable(基于所有影响UI的属性):
struct ChatViewModel: Hashable { let chat: Chat let tags: [Tag] let status: ChatStatus // 哈希和相等判断要包含所有会影响UI展示的属性 func hash(into hasher: inout Hasher) { hasher.combine(chat.id) hasher.combine(tags) // 前提是Tag实现了Hashable hasher.combine(status) // 前提是ChatStatus实现了Hashable } static func == (lhs: Self, rhs: Self) -> Bool { return lhs.chat.id == rhs.chat.id && lhs.tags == rhs.tags && lhs.status == rhs.status } }
3. 重构DiffableDataSource配置
更新数据源类型,同时修改Cell配置逻辑,让Cell只接收ViewModel:
// 更新数据源定义 private var dataSource: UICollectionViewDiffableDataSource<Sections, CollectionItem>? // 初始化数据源时的Cell Provider dataSource = UICollectionViewDiffableDataSource<Sections, CollectionItem>(collectionView: collectionView) { [weak self] collectionView, indexPath, item in guard let self = self else { return nil } switch item { case .user(let user): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UserCell.reuseID, for: indexPath) as! UserCell cell.configure(with: user) return cell case .chat(let vm): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ChatCell.reuseID, for: indexPath) as! ChatCell cell.configure(with: vm) // 现在Cell不需要依赖外部Manager了 return cell } }
4. 重构快照生成与更新逻辑
生成快照时,把Chat转换成包含所有依赖数据的ViewModel;当外部数据变化时,更新对应的ViewModel并调用原生的reconfigureItems:
// 生成完整快照 private func createFullSnapshot() -> NSDiffableDataSourceSnapshot<Sections, CollectionItem> { var snapshot = NSDiffableDataSourceSnapshot<Sections, CollectionItem>() snapshot.appendSections([.users, .chats]) // 添加User项 let userItems = self.users.map { CollectionItem.user($0) } snapshot.appendItems(userItems, toSection: .users) // 添加Chat项,整合外部数据 let chatItems = self.chats.map { chat in let tags = self.tagsManager.tags(for: chat.id) let status = self.statusManager.status(for: chat.id) return CollectionItem.chat(ChatViewModel(chat: chat, tags: tags, status: status)) } snapshot.appendItems(chatItems, toSection: .chats) return snapshot } // 局部更新某个Chat的标签/状态 func updateChatExternalData(for chatId: String) { guard let targetChat = chats.first(where: { $0.id == chatId }) else { return } // 生成更新后的ViewModel let updatedTags = tagsManager.tags(for: chatId) let updatedStatus = statusManager.status(for: chatId) let updatedVM = ChatViewModel(chat: targetChat, tags: updatedTags, status: updatedStatus) let updatedItem = CollectionItem.chat(updatedVM) // 用原生方法更新 var currentSnapshot = dataSource?.snapshot() ?? createFullSnapshot() if currentSnapshot.contains(updatedItem) { currentSnapshot.reconfigureItems([updatedItem]) dataSource?.apply(currentSnapshot, animatingDifferences: true) } } // 全量更新方法保留(用于数据源整体变化时) func reloadDiffableDataSource() { let snapshot = createFullSnapshot() dataSource?.apply(snapshot, animatingDifferences: true) }
额外注意事项
- 确保
User、Tag、ChatStatus都正确实现了Hashable,否则ViewModel的哈希判断会失效; - 给
tagsManager、statusManager添加变化通知(比如用Combine、Delegate或者闭包回调),在ViewController里监听这些通知,触发对应的updateChatExternalData方法; - 彻底移除ChatCell对外部Manager的依赖,让Cell只负责接收ViewModel并展示,实现数据驱动UI。
内容的提问来源于stack exchange,提问作者PavelPud
相关产品推荐
相关产品推荐

