Core data 作为NSFetchedResultsControllerDelegate处理删除的行时,为什么NSTableView会崩溃?

Core data 作为NSFetchedResultsControllerDelegate处理删除的行时,为什么NSTableView会崩溃?,core-data,nsfetchedresultscontroller,nstableview,appkit,Core Data,Nsfetchedresultscontroller,Nstableview,Appkit,我使用的是相当标准的NSTableView+CoreData+NSFetchedResultsController设置,相关的视图控制器为NSFetchedResultsController委托以接收更改。以下是来自视图控制器的相关代码位: func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: Inde

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

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")
}
最后一行导致崩溃:

2019-05-06 22:01:30.968849+0300 MyApp[3517:598234]***由于未捕获的异常“NSTableViewException”而终止应用程序,原因:“NSTableView错误插入/删除/移动第3行(numberOfRows:1)。”

前三次删除碰巧是按“正确”的顺序报告的(首先删除索引较大[行号]的行)。最后一行出现“无序”,其他行此时似乎已经从NSTableView中消失

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

@objc func syncContextDidSave(note: NSNotification) {
    viewContext.perform {
        self.viewContext.mergeChanges(fromContextDidSave: note as Notification)
    }
}

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

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

在下面的示例中,删除操作将始终首先运行,从而推迟删除操作,但其目的是在开始和结束之间同时提交所有操作,以便UITableView可以为您完成繁重的工作

- (IBAction)insertAndDeleteRows:(id)sender {
    // original rows: Arizona, California, Delaware, New Jersey, Washington

    [states removeObjectAtIndex:4]; // Washington
    [states removeObjectAtIndex:2]; // Delaware
    [states insertObject:@"Alaska" atIndex:0];
    [states insertObject:@"Georgia" atIndex:3];
    [states insertObject:@"Virginia" atIndex:5];

    NSArray *deleteIndexPaths = [NSArray arrayWithObjects:
                                [NSIndexPath indexPathForRow:2 inSection:0],
                                [NSIndexPath indexPathForRow:4 inSection:0],
                                nil];
    NSArray *insertIndexPaths = [NSArray arrayWithObjects:
                                [NSIndexPath indexPathForRow:0 inSection:0],
                                [NSIndexPath indexPathForRow:3 inSection:0],
                                [NSIndexPath indexPathForRow:5 inSection:0],
                                nil];
    UITableView *tv = (UITableView *)self.view;

    [tv beginUpdates];
    [tv insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
    [tv deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
    [tv endUpdates];

    // ending rows: Alaska, Arizona, California, Georgia, New Jersey, Virginia
}
此示例从数组中删除两个字符串(及其 并将三个字符串插入数组(沿 以及相应的行)。下一节,排序 操作和索引路径,解释行的特定方面(或 节)插入和删除行为

这里的关键是在开始和结束更新调用之间同时传递所有删除索引。如果先存储索引,然后传入,那么就会遇到我在评论中提到的情况,即索引开始抛出越界异常

苹果和上面标题下的示例:“批量插入和删除操作的示例”


希望这有助于为您指明正确的方向。

从fetchedResultsController更新要比Apple文档说明的更困难。当同时存在移动和插入或移动和删除时,您共享的代码将导致此类错误。对于您的案例来说,这似乎不是正在发生的事情,但是这个设置也会修复它

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

对于更新,您不关心它在插入和删除之前的位置-仅在插入和删除之后-因此使用
newindepath
而不是
indepath
。这将修复当您同时更新和插入(或更新和删除)单元格而单元格未按预期更新时可能发生的错误

对于
move
,代表要说明它在插入之前从何处移动,以及在插入和删除之后应该插入到何处。当您进行移动和插入(或移动和删除)时,这可能是一个挑战。您可以通过将来自
controller:didChangeObject:atIndexPath:forChangeType:newindexath:
的所有更改保存到三个不同的数组中,插入、删除和更新来修复此问题。当您得到一个
move
时,在insert数组和delete数组中为它添加一个条目。在
controllerDidChangeContent:
中,按降序排列删除数组,按升序排列插入数组。然后应用更改—首先删除,然后插入,然后更新。这将修复在移动和插入(或移动和删除)的同时发生的崩溃

我无法解释为什么你有一个无序删除。在我的测试中,我总是看到删除服务是降序的,插入服务是升序的。不过,此设置也会解决您的问题,因为有一个步骤可以对删除进行排序

如果有节,则还可以将节更改保存在数组中,然后按以下顺序应用更改:删除(降序)、节删除(降序)、节插入(升序)、插入(升序)、更新(任意顺序)。无法移动或更新节

总结:

  • 有5个数组:sectionInserts、sectionDeletes、rowDeletes、rowInserts和RowUpdate

  • 在controllerWillChangeContent中,清除所有数组

  • 在控制器中:didChangeObject:将索引路径添加到数组中(移动是删除和插入。更新使用newindexath)

  • 在控制器中:didChangeSection将节添加到sectionInserts或rowDeletes数组中

  • 在controllerDidChangeContent中:按如下方式处理它们:

    • 按降序对行进行排序
    • 排序部分删除降序
    • 按升序排序
    • 按升序排序行插入
  • 然后在一个performBatchUpdates块中,将更改应用于collectionView:按该顺序执行rowDeletes、sectionDelete、sectionInserts、rowInserts和RowUpdate


  • BeginUpdate/endupdates对动画进行分组,而不是
    removeRows
    s。如何从上下文中删除对象?
    anObject
    是否匹配
    indepath
    ?对象删除由另一个
    NSManagedObjectContext
    再次触发
    - (IBAction)insertAndDeleteRows:(id)sender {
        // original rows: Arizona, California, Delaware, New Jersey, Washington
    
        [states removeObjectAtIndex:4]; // Washington
        [states removeObjectAtIndex:2]; // Delaware
        [states insertObject:@"Alaska" atIndex:0];
        [states insertObject:@"Georgia" atIndex:3];
        [states insertObject:@"Virginia" atIndex:5];
    
        NSArray *deleteIndexPaths = [NSArray arrayWithObjects:
                                    [NSIndexPath indexPathForRow:2 inSection:0],
                                    [NSIndexPath indexPathForRow:4 inSection:0],
                                    nil];
        NSArray *insertIndexPaths = [NSArray arrayWithObjects:
                                    [NSIndexPath indexPathForRow:0 inSection:0],
                                    [NSIndexPath indexPathForRow:3 inSection:0],
                                    [NSIndexPath indexPathForRow:5 inSection:0],
                                    nil];
        UITableView *tv = (UITableView *)self.view;
    
        [tv beginUpdates];
        [tv insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
        [tv deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
        [tv endUpdates];
    
        // ending rows: Alaska, Arizona, California, Georgia, New Jersey, Virginia
    }