iOS Swift中UITableView使用拖拽功能时触发NSRangeException崩溃
问题现象
实现UITableView行拖拽重排功能时运行触发崩溃,崩溃日志如下:
Terminating app due to uncaught exception 'NSRangeException', reason: 'Attempted to scroll the table view to an out-of-bounds row (3) when there are only 1 rows in section 0.
报错截图:
现有代码
ViewDidLoad配置
tableView.dragDelegate = self tableView.dragInteractionEnabled = true tableView.dataSource = self tableView.delegate = self
已实现的代理方法
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { let dragItem = UIDragItem(itemProvider: NSItemProvider()) // dragItem.localObject = data[indexPath.row] return [ dragItem ] } func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { // 此处已编写本地数据库更新逻辑 self.tableView.reloadData() print("Drag from :\(sourceIndexPath.section) and Record Number:\(sourceIndexPath.row) TO :\(destinationIndexPath.section)") }
预期效果
- 拖拽前效果:

- 拖拽完成后预期行顺序更新效果:

- 已在
moveRowAt代理方法中正确更新本地数据库记录,操作完成后调用了tableView.reloadData(),但拖拽操作时仍触发上述越界崩溃。
崩溃原因
- 核心触发点:在
tableView(_:moveRowAt:to:)回调执行时直接调用reloadData()。该回调触发时系统拖拽交互动画尚未结束,UITableView内部仍持有拖拽流程对应的索引路径状态,此时全量刷新表会导致系统持有的旧索引(如报错中的row=3)和刷新后实际的行数(section0仅1行)不匹配,触发越界崩溃。 - 缺失必要逻辑:注释了
dragItem.localObject的对象绑定,未实现tableView(_:canMoveRowAt:)数据源方法,也未配置Drop代理,系统无法正确识别拖拽行的状态、完成本地移动操作。
修复方案
- 移除
moveRowAt方法中的self.tableView.reloadData()调用:拖拽重排完成后系统会自动更新单元格位置,无需手动全量刷新。 - 在
moveRowAt方法中优先同步内存中的数据源,再执行数据库更新,保证数据源和UI状态一致:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { // 先更新内存中的数据源数组 let movedItem = data.remove(at: sourceIndexPath.row) data.insert(movedItem, at: destinationIndexPath.row) // 再执行本地数据库更新逻辑 // 原有数据库更新代码保留... print("Drag from :\(sourceIndexPath.section) and Record Number:\(sourceIndexPath.row) TO :\(destinationIndexPath.section)") }
- 补全拖拽相关配置:
- 在ViewDidLoad中增加
tableView.dropDelegate = self配置 - 取消
itemsForBeginning方法中dragItem.localObject的注释,绑定当前拖拽的数据对象 - 实现
canMoveRowAt数据源方法,允许行移动 - 实现Drop代理方法,支持当前tableView内的移动操作
- 在ViewDidLoad中增加
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { // 根据业务需求返回可移动的行,默认全部可移动直接返回true return true } func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { let dragItem = UIDragItem(itemProvider: NSItemProvider()) dragItem.localObject = data[indexPath.row] return [ dragItem ] } func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { // 指定仅支持当前tableView内的行移动操作 return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) } func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { // 重排逻辑已在moveRowAt中处理,无需额外代码 }
- 特殊场景说明:如果拖拽完成后确实需要刷新UI,不要直接调用
reloadData(),可将刷新逻辑异步派发到主线程,等系统拖拽流程完全结束后再执行,或使用performBatchUpdates做增量更新,避免打断系统拖拽状态。
内容的提问来源于stack exchange,提问作者Mr.Javed Multani
相关产品推荐
相关产品推荐

