为什么将删除的行作为 NSFetchedResultsControllerDelegate 处理时 NSTableView 会崩溃? [英] Why does NSTableView crash when processing deleted rows as NSFetchedResultsControllerDelegate?

查看:17
本文介绍了为什么将删除的行作为 NSFetchedResultsControllerDelegate 处理时 NSTableView 会崩溃?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 NSTableView + CoreData + NSFetchedResultsController 的相当标准的设置,相关的视图控制器是 NSFetchedResultsControllerDelegate 来接收更改.以下是来自视图控制器的相关代码:

I am using a fairly standard setup of NSTableView + CoreData + NSFetchedResultsController, with the relevant view controller being NSFetchedResultsControllerDelegate to receive the changes. Here are the relevant bits of code from the view controller:

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

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

I understand that I should be able to batch all the updates this way, even if multiple rows are deleted. However, this causes a crash with multiple deletes in a row.

这是一个会话的日志输出,表格最初有四行,所有这些都被删除了:

Here is the log output from a session, with the table initially having four rows, and all of them being deleted:

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] *** 由于未捕获的异常NSTableViewException"而终止应用程序,原因:NSTableView 错误插入/删除/移动第 3 行(numberOfRows: 1).'

前三个删除碰巧按正确"顺序报告(首先删除具有较大索引 [行号] 的行).最后一行乱序"到达,此时其他行似乎已经从 NSTableView 中消失了.

The first three deletes happen to be reported in the "right" order (rows with bigger indexes [row numbers] being deleted first). The last one arrives "out of order" and the other rows are seemingly already gone from NSTableView by this time.

首先如何从上下文中删除对象:我使用推荐的最佳实践,让两个托管对象上下文针对同一个 NSPersistentContainer 工作,一个用于主线程中的 UI 工作,一个用于后台/网络工作.他们观察着彼此的变化.当同步上下文从网络接收到一些更改,保存它们,并将它们传播到视图上下文时,会触发此崩溃,在应用程序的其他地方使用此方法:

How are objects deleted from the context in the first place: I am using the recommended best practice of having two Managed Object Contexts work against the same NSPersistentContainer, one for UI work in the main thread, and one for background/network work in background. They watch each other’s changes. This crash is triggered when the sync context receives some changes from the network, saves them, and they propagate to view context, with this method elsewhere in the app:

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

我是否误解了如何使用获取的结果控制器委托?我认为 beginupdates/endupdates 调用确保表视图模型"不会在它们之间改变?我应该怎么做才能消除崩溃?

Have I misunderstood how to work with the fetched results controller delegate? I thought that the beginupdates/endupdates calls make sure that the "table view model" does not change between them? What should I do to eliminate the crash?

推荐答案

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

Updating from a fetchedResultsController is more difficult than the Apple documentation states. The code that you are sharing will cause this kinds of a bug when there is a move and an insert or a move and delete at the same time. That doesn't seem to be what is going on for your case, but this setup will fix it as well.

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

indexPath is the index BEFORE the deletes and inserts are applied; newIndexPath is the index AFTER the deletes and inserts are applied.

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

For updates you don't care where it was BEFORE the inserts and delete - only after - so use newIndexPath not indexPath. This will fix bugs that can happen when you an update and insert (or update and delete) at the same time and the cell doesn't update as you expect.

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

For move the delegate is saying where it moved from BEFORE the inserts and where it should be inserted AFTER the inserts and deletes. This can be challenging when you have a move and insert (or move and delete). You can fix this by saving all the changes from controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: into three different arrays, insert, delete and update. When you get a move add an entry for it in both the insert array and in the delete array. In controllerDidChangeContent: sort the delete array descending and the insert array ascending. Then apply the changes - first delete, then insert, then update. This will fix crashes that can happens when you have a move and insert (or move and delete) at the same time.

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

I can't explain why you have an out of order delete. In my testing I have always seen deletes are served descending and inserts are served ascending. Nevertheless this setup will fix your issues as well, as there is a step to sort the deletes.

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

If you have sections then also save the sections changes in arrays, and then apply the changes in order: deletes (descending), sectionDelete (descending), sectionInserts (ascending), inserts(ascending), updates (any order). Sections can't move or be updated.

总结:

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

  1. Have 5 arrays : sectionInserts, sectionDeletes, rowDeletes, rowInserts, and rowUpdates

在 controllerWillChangeContent 中清除所有数组

in controllerWillChangeContent clear all the arrays

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

in controller:didChangeObject: add the indexPaths into the arrays (move is a delete and an insert. Update uses newIndexPath)

在 controller:didChangeSection 中将该部分添加到 sectionInserts 或 rowDeletes 数组中

in controller:didChangeSection add the section into the sectionInserts or rowDeletes array

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

in controllerDidChangeContent: process them as follows:

  • 排序 rowDeletes 降序
  • 排序部分删除降序
  • 排序部分按升序插入
  • 按升序排列行插入

然后在一个 performBatchUpdates 块中将更改应用到 collectionView:按顺序 rowDeletes、sectionDelete、sectionInserts、rowInserts 和 rowUpdates.

then in one performBatchUpdates block apply the changes to the collectionView: rowDeletes, sectionDelete, sectionInserts, rowInserts and rowUpdates in that order.

这篇关于为什么将删除的行作为 NSFetchedResultsControllerDelegate 处理时 NSTableView 会崩溃?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆