使用UITableViewDiffableDataSource拖拽移动Cell致App崩溃求助
你遇到的这个崩溃是DiffableDataSource的核心规则冲突导致的——当你用UITableViewDiffableDataSource作为TableView的数据源时,所有UI更新必须通过它的Snapshot API完成,绝对不能直接调用UITableView的moveRow/insertRows这类原生修改方法,而默认的拖放逻辑恰好会尝试直接操作TableView,触发系统抛出NSInternalInconsistencyException。
崩溃原因详解
崩溃日志已经明确给出了错误原因:
UITableView must be updated via the UITableViewDiffableDataSource APIs when acting as the UITableView's dataSource: please do not call mutation APIs directly on UITableView.
DiffableDataSource的设计逻辑是用不可变的NSDiffableDataSourceSnapshot来维护数据状态,任何数据变更都要通过生成新Snapshot并调用apply(_:animatingDifferences:)来同步到UI,拖放操作也必须遵循这个规则。
具体修复步骤
我们需要修改拖放相关代码,手动处理数据移动,再通过Snapshot更新UI:
1. 先维护一个可修改的数据源数组
首先你需要持有一个存储Item的可变数组(比如private var items: [Item] = []),因为DiffableDataSource的Snapshot是只读的,我们需要先修改这个数组,再生成新的Snapshot同步到UI。
2. 完善DragDelegate,传递拖拽的源信息
原来的拖拽方法只是返回空的ItemProvider,我们需要把拖拽的源位置关联进去,方便后续拖放时获取:
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { let item = items[indexPath.row] // 本地拖拽用localObject传递数据更高效,不需要复杂的ItemProvider处理 let dragItem = UIDragItem(itemProvider: NSItemProvider()) dragItem.localObject = indexPath return [dragItem] }
3. 核心修复:重写performDropWith方法
这是解决崩溃的关键,我们在这里处理数据移动逻辑,再通过Snapshot更新UI:
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { guard let destinationIndexPath = coordinator.destinationIndexPath else { return } // 只处理本地拖拽的情况 for item in coordinator.items { guard let sourceIndexPath = item.dragItem.localObject as? IndexPath, sourceIndexPath != destinationIndexPath else { continue } // 第一步:修改本地数据源数组 let movedItem = items.remove(at: sourceIndexPath.row) items.insert(movedItem, at: destinationIndexPath.row) // 第二步:生成新的Snapshot并应用到DiffableDataSource var snapshot = dataSource.snapshot() // 先删除再插入,触发移动动画 snapshot.deleteItems([movedItem]) snapshot.insertItems([movedItem], beforeItem: items[destinationIndexPath.row]) dataSource.apply(snapshot, animatingDifferences: true) // 第三步:完成拖放的动画协调 coordinator.drop(item.dragItem, toRowAt: destinationIndexPath) } }
4. 优化拖放提案的判断逻辑
可以在dropSessionDidUpdate里补充更严格的判断,确保只处理当前TableView的本地拖拽:
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { guard session.localDragSession != nil, tableView.dataSource is UITableViewDiffableDataSource<Section, Item> else { return UITableViewDropProposal(operation: .cancel) } return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) }
关键注意事项
- 永远不要直接调用UITableView的
moveRow(at:to:)、insertRows(at:with:)等方法,所有UI更新必须通过DiffableDataSource的Snapshot完成。 - 如果你的数据来自网络,每次修改后要同步更新本地的数据源数组,再生成Snapshot。
- 本地拖拽时,用
dragItem.localObject传递数据比用NSItemProvider更简单高效,不需要处理对象的序列化/反序列化。
内容的提问来源于stack exchange,提问作者user25917

