当将已删除的行作为NSFetchedResultsControllerDelegate处理时,NSTableView为什么会崩溃?

Jaa*_*nus 8 core-data nstableview appkit nsfetchedresultscontroller

我使用的是NSTableView + CoreData + NSFetchedResultsController的相当标准的设置,相关的视图控制器为NSFetchedResultsControllerDelegate来接收更改。这是来自视图控制器的相关代码位:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?){

    print("Change type \(type) for indexPath \(String(describing: indexPath)), newIndexPath \(String(describing: newIndexPath)). Changed object: \(anObject). FRC by this moment has \(String(describing: self.frc?.fetchedObjects?.count)) objects, tableView has \(self.tableView.numberOfRows) rows")

    switch type {
    case .insert:
        if let newIndexPath = newIndexPath {
            tableView.insertRows(at: [newIndexPath.item], withAnimation: .effectFade)
        }
    case .delete:
        if let indexPath = indexPath {
            tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade)
        }
    case .update:
        if let indexPath = indexPath {
            let row = indexPath.item
            for column in 0..<tableView.numberOfColumns {
                tableView.reloadData(forRowIndexes: IndexSet(integer: row), columnIndexes: IndexSet(integer: column))
            }
        }
    case .move:
        if let indexPath = indexPath, let newIndexPath = newIndexPath {
            tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade)
            tableView.insertRows(at: [newIndexPath.item], withAnimation: .effectFade)
        }
    @unknown default:
        fatalError("Unknown fetched results controller change result type")
    }
}

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    print("tableViewBeginUpdates")
    tableView.beginUpdates()
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
    print("tableViewEndUpdates")
}
Run Code Online (Sandbox Code Playgroud)

我了解即使删除多行,我也应该能够以这种方式批处理所有更新。但是,这会导致崩溃,并导致连续多次删除。

这是会话的日志输出,该表最初具有四行,并且所有行均被删除:

tableViewBeginUpdates
Change type NSFetchedResultsChangeType for indexPath Optional([0, 2]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 4 rows
Change type NSFetchedResultsChangeType for indexPath Optional([0, 1]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 3 rows
Change type NSFetchedResultsChangeType for indexPath Optional([0, 0]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 2 rows
Change type NSFetchedResultsChangeType for indexPath Optional([0, 3]), newIndexPath nil. Changed object: /… correct object info …/. FRC by this moment has Optional(0) objects, tableView has 1 rows
Run Code Online (Sandbox Code Playgroud)

最后一行导致崩溃:

2019-05-06 22:01:30.968849+0300 MyApp[3517:598234] *** Terminating app due to uncaught exception 'NSTableViewException', reason: 'NSTableView error inserting/removing/moving row 3 (numberOfRows: 1).'

前三个删除恰好以“正确”的顺序报告(具有较大索引[行号]的行首先被删除)。最后一个到达“乱序”,其他行似乎已经从NSTableView消失了。

首先如何从上下文中删除对象:我正在使用建议的最佳实践,即针对同一个NSPersistentContainer使用两个托管对象上下文,其中一个用于主线程中的UI,而一个用于后台中的后台/网络。他们看着对方的变化。当同步上下文从网络接收到一些更改并将其保存并传播到视图上下文时,使用应用程序中其他位置的此方法,将触发此崩溃:

@objc func syncContextDidSave(note: NSNotification) {
    viewContext.perform {
        self.viewContext.mergeChanges(fromContextDidSave: note as Notification)
    }
}
Run Code Online (Sandbox Code Playgroud)

我是否误解了如何使用获取的结果控制器委托?我以为beginupdates / endupdates调用可确保“表视图模型”在它们之间不发生变化?我应该怎么做才能消除撞车事故?

Jon*_*ose 12

从 fetchedResultsController 更新比 Apple 文档说明的更困难。当同时进行移动和插入或移动和删除时,您共享的代码将导致此类错误。您的情况似乎不是这样,但是此设置也可以解决此问题。

indexPath 是应用删除和插入之前的索引; newIndexPath是应用删除和插入后的索引。

对于更新,你不关心它在插入和删除之前的位置 - 只有之后 - 所以newIndexPath不要使用indexPath。这将修复当您同时更新和插入(或更新和删除)并且单元格没有按预期更新时可能发生的错误。

因为move代表说它从插入之前移动到哪里,以及应该在插入和删除之后插入到哪里。当您进行移动和插入(或移动和删除)时,这可能具有挑战性。您可以通过将所有更改保存controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:到三个不同的数组(插入、删除和更新)中来解决此问题。当您move在插入数组和删除数组中为它添加一个条目时。在controllerDidChangeContent:排序中删除数组降序和插入数组升序。然后应用更改 - 首先删除,然后插入,然后更新。这将修复当您同时进行移动和插入(或移动和删除)时可能发生的崩溃。

我无法解释为什么你有一个乱序删除。在我的测试中,我一直看到删除是降序的,插入是升序的。尽管如此,此设置也将解决您的问题,因为有一个步骤可以对删除进行排序。

如果您有节,则还将节更改保存在数组中,然后按顺序应用更改:删除(降序)、节删除(降序)、节插入(升序)、插入(升序)、更新(任何顺序)。部分不能移动或更新。

概括:

  1. 有 5 个数组:sectionInserts、sectionDeletes、rowDeletes、rowInserts 和 rowUpdates

  2. 在 controllerWillChangeContent 中清除所有数组

  3. 在控制器中:didChangeObject:将 indexPaths 添加到数组中(移动是删除和插入。更新使用 newIndexPath)

  4. 在控制器中:didChangeSection 将该部分添加到 sectionInserts 或 rowDeletes 数组中

  5. 在 controllerDidChangeContent: 中处理它们如下:

    • 排序 rowDeletes 降序
    • 排序部分删除降序
    • 排序部分升序插入
    • sort rowInserts 升序
  6. 然后在一个 performBatchUpdates 块中将更改应用到 collectionView:rowDeletes、sectionDelete、sectionInserts、rowInserts 和 rowUpdates 的顺序。

  • 谢谢你。这似乎有效。将它作为一个单独的对象来实现和测试,因为我可以对它进行干净的单元测试。发布在这里:https://gist.github.com/jaanus/c28fa29ba24d44e1a2fff2d62e0493e6 (2认同)
  • 我有一个简单的项目来实现这个。可以在这里找到:https://github.com/jon513/FetchedResultsControllerNeverCrash (2认同)