Swift UITableView移动行报错:尝试移动至不存在的索引路径
嘿,我帮你梳理下这个问题的根源和解决办法,你遇到的两个错误都是没搞清楚NSFetchedResultsController(简称FRC)和UITableView之间的协作逻辑~
第一个错误:"Attempt to move index path to index path that does not exist"
你直接调用tableView.moveRowAtIndexPath是错误的核心原因!因为当使用FRC时,UITableView的数据源完全由FRC提供,你不能直接操作UI层的行移动——此时FRC里的数据还没变化,目标section里根本没有对应的行,自然会报错说目标indexPath不存在。
第二个错误:行数不匹配的更新异常
你手动调用controller(_:didChangeObject:...)的操作完全搞错了方向,这个方法是FRC自己用来通知代理数据变化的,不是让我们手动调用的。你手动执行delete+insert,但FRC的数据源(Core Data里的数据)并没有被修改,导致tableView的行数计算和实际数据源不一致,触发了系统的更新校验错误。
正确的解决流程(基于FRC+Core Data)
核心思路是:先修改Core Data对象中区分section的属性,保存上下文后让FRC自动触发tableView的更新,而不是直接操作UI。
步骤1:确保FRC的Section划分正确
假设你的两个section是根据对象的某个布尔属性(比如isArchived,false在section0,true在section1)来划分的,那FRC的初始化要指定sectionNameKeyPath:
// 示例:初始化FRC时指定section划分的属性 let fetchRequest: NSFetchRequest<YourManagedObject> = YourManagedObject.fetchRequest() // 先按section属性排序,再按其他业务字段排序 fetchRequest.sortDescriptors = [NSSortDescriptor(key: "isArchived", ascending: true), NSSortDescriptor(key: "createDate", ascending: false)] let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: yourManagedContext, sectionNameKeyPath: "isArchived", cacheName: nil) frc.delegate = self
步骤2:修改moveRow方法的逻辑
在moveRowAt方法里,不要操作tableView,而是修改移动对象的section属性,然后保存上下文:
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { // 获取要移动的Core Data对象 let movedObject = fetchedResultsController.object(at: sourceIndexPath) as! YourManagedObject // 根据目标section修改对应的属性(这里假设section1对应isArchived=true) movedObject.isArchived = (destinationIndexPath.section == 1) // 保存上下文,触发FRC检测数据变化 do { try movedObject.managedObjectContext?.save() } catch { print("保存上下文失败:\(error.localizedDescription)") } }
步骤3:正确实现FRC的代理方法
确保你已经实现了FRC的三个核心代理方法,让它自动更新tableView:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.beginUpdates() } func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .delete: if let indexPath = indexPath { tableView.deleteRows(at: [indexPath], with: .automatic) } case .insert: if let newIndexPath = newIndexPath { tableView.insertRows(at: [newIndexPath], with: .automatic) } case .move: if let indexPath = indexPath, let newIndexPath = newIndexPath { tableView.moveRow(at: indexPath, to: newIndexPath) } case .update: if let indexPath = indexPath { tableView.reloadRows(at: [indexPath], with: .automatic) } @unknown default: break } } func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.endUpdates() }
为什么这个方法能解决问题?
当你修改Core Data对象的属性并保存后,FRC会自动检测到数据变化,通过代理方法通知tableView执行对应的更新(这里会触发.move类型的变化,或者.delete+.insert,取决于你的排序规则),此时数据源和UI完全同步,不会出现indexPath不存在或行数不匹配的问题。
内容的提问来源于stack exchange,提问作者Nathaniel

