Swift中使用iOS11拖放协议实现CollectionView多元素批量重排问题
iOS 11 CollectionView 多选拖放重排解决方案
我太懂这种卡了两周的崩溃感!之前做项目时也在iOS 11的拖放多选重排上踩了一堆坑,终于摸出了稳定可行的方案,给你拆解一下关键步骤:
1. 先开启多选基础配置
首先得让CollectionView支持多选,这是实现的前提:
collectionView.allowsMultipleSelection = true // 可选:给cell加选中状态的视觉反馈,让操作更直观 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let cell = collectionView.cellForItem(at: indexPath) cell?.backgroundColor = .lightGray // 选中高亮 } func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { let cell = collectionView.cellForItem(at: indexPath) cell?.backgroundColor = .white // 取消选中恢复原样 }
2. 拖拽会话:主动收集所有选中元素
默认的拖拽代理只会返回当前触摸的单个元素,我们要在这里主动抓取所有选中的indexPaths,生成对应的拖拽项:
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { // 如果用户没提前选中就拖拽,自动选中当前触摸的元素 if !collectionView.indexPathsForSelectedItems?.contains(indexPath) ?? true { collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) } // 获取所有选中的indexPaths并排序,避免后续数据乱序 guard let selectedIndexPaths = collectionView.indexPathsForSelectedItems?.sorted() else { return [] } // 为每个选中元素创建DragItem,并绑定原indexPath方便后续识别 var dragItems = [UIDragItem]() for path in selectedIndexPaths { let item = yourDataSource[path.item] // 替换成你的数据源模型 let itemProvider = NSItemProvider(object: item as! NSItemProviderWriting) // 模型需遵循NSItemProviderWriting协议 let dragItem = UIDragItem(itemProvider: itemProvider) dragItem.localObject = path // 绑定原位置信息 dragItems.append(dragItem) } // 拖拽开始时临时隐藏选中的cell,优化视觉体验 selectedIndexPaths.forEach { collectionView.cellForItem(at: $0)?.isHidden = true } return dragItems }
3. 拖放结束:批量更新数据源与UI
这是最容易出错的环节,重点要处理indexPath偏移问题,按顺序操作数据源:
func collectionView(_ collectionView: UICollectionView, performDropWithCoordinator coordinator: UICollectionViewDropCoordinator) { guard let destinationIndexPath = coordinator.destinationIndexPath else { // 拖放位置无效,恢复隐藏的cell collectionView.indexPathsForSelectedItems?.forEach { collectionView.cellForItem(at: $0)?.isHidden = false } return } // 取出拖拽项绑定的原indexPath let dragItems = coordinator.items.compactMap { $0.dragItem.localObject as? IndexPath } guard !dragItems.isEmpty else { return } // 按原顺序取出要移动的元素 let itemsToMove = dragItems.sorted().map { yourDataSource[$0.item] } // 从数据源移除元素:必须从后往前删,避免前面的元素删除后导致后面的indexPath偏移 dragItems.sorted(by: { $0.item > $1.item }).forEach { yourDataSource.remove(at: $0.item) } // 调整插入位置:如果目标位置在原选中元素范围内,要扣除已删除元素的数量 let adjustedDestinationIndex = destinationIndexPath.item - dragItems.filter { $0.item < destinationIndexPath.item }.count // 插入元素到新位置 yourDataSource.insert(contentsOf: itemsToMove, at: adjustedDestinationIndex) // 批量更新CollectionView,同步UI与数据源 collectionView.performBatchUpdates({ // 删除原选中项 collectionView.deleteItems(at: dragItems) // 插入新位置的项 let newIndexPaths = (adjustedDestinationIndex..<adjustedDestinationIndex+itemsToMove.count).map { IndexPath(item: $0, section: destinationIndexPath.section) } collectionView.insertItems(at: newIndexPaths) }, completion: { _ in // 更新选中状态,选中新位置的元素 collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) } newIndexPaths.forEach { collectionView.selectItem(at: $0, animated: true, scrollPosition: []) } }) // 完成拖放动画 coordinator.drop(coordinator.items.first?.dragItem, toItemAt: destinationIndexPath) }
4. 避坑关键提醒
- 数据源操作顺序:先删后插,且删除时从后往前,彻底避免indexPath偏移导致的数据错乱。
- 无需占位符:你之前用占位符出错是正常的,它本来是为远程异步加载设计的,本地多选重排直接操作本地数据源+批量更新就足够。
- 绑定原位置:通过
dragItem.localObject绑定原indexPath,能在拖放时精准识别要移动的元素,不会搞混数据。
内容的提问来源于stack exchange,提问作者lozflan
相关产品推荐
相关产品推荐

