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

时间:2019-05-03 19:45:15

标签: core-data nsfetchedresultscontroller nstableview appkit

我使用的是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")
}

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

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

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

最后一行会导致崩溃:

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)
    }
}

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

2 个答案:

答案 0 :(得分:2)

希望有关UITableView上的批量删除操作的官方文档可能会对您有所帮助。

在下面的示例中,删除操作将始终首先运行,将删除操作推迟,但其想法是您在开始和结束之间同时提交所有操作,以便UITableView可以为你。

[
    { code: "AAA", level: 4 },
    { code: "BBB", level: 4 }
]
  

此示例从数组中删除了两个字符串(以及它们的   相应的行),然后将三个字符串插入数组(以及   及其相应的行)。下一节,订购   操作和索引路径,说明行的特定方面(或   部分)的插入和删除行为。

这里的关键是在开始和结束更新调用之间同时传递所有删除索引。如果先存储索引,然后再传递它们,那么您将遇到我在评论中提到的情况,其中索引开始超出范围例外。

苹果documentation can be found here,以及位于标题下的上述示例:“批量插入和删除操作的示例”

希望这可以帮助您指出正确的方向。

答案 1 :(得分:1)

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

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

对于更新,您不在乎插入和删除之前(仅在之后)在哪里,因此请使用newIndexPath而不是indexPath。这样可以解决当您同时进行更新和插入(或更新和删除)并且单元格未按预期更新时可能发生的崩溃。

对于move,代表说的是插入之前的位置以及插入和删除之后应将其插入的位置。当您进行移动和插入(或移动和删除)时,这可能会具有挑战性。您可以通过以下方法解决此问题:将controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:的所有更改保存到三个不同的数组中,然后进行插入,删除和更新(可以使用自定义对象或字典-可以使用任何合适的方法)。得到move后,在插入数组和删除数组中都添加一个条目。在controllerDidChangeContent:中,将删除数组降序,将插入数组升序。然后应用更改-首先删除,然后插入,然后更新。这样可以解决当您同时移动并插入(或移动并删除)时可能发生的崩溃。

我无法解释为什么您删除了乱序的内容。在我的测试中,我一直看到删除以降序排列,插入以升序排列。不过,此设置也可以解决您的问题,因为有一个设置可以对删除内容进行排序。

如果有节,则还将节更改保存在数组中,然后按顺序应用更改:删除(降序),sectionDelete(降序),sectionInserts(升序),inserts(升序),更新(任何顺序)。栏目无法移动或更新。

摘要:

  1. 具有5个数组:sectionInserts,sectionDeletes,rowDeletes,rowInserts和rowUpdates
  2. 在controllerWillChangeContent中清除所有数组
  3. 在controller:didChangeObject中:将indexPaths添加到数组中(移动是删除和插入)
  4. 在controller:didChangeSection中将该部分添加到sectionInserts或rowDeletes数组中
  5. 在controllerDidChangeContent中的
  6. :按以下步骤处理它们:

    • 排序行删除降序
    • 对section进行降序删除
    • sort部分插入升序
    • 对rowInserts进行升序排序
  7. 然后在一个performBatchUpdates块中将更改应用到collectionView:rowDeletes,sectionDelete,sectionInserts,rowInserts和rowUpdates依次。