当我刷新核心数据实体时,UICollectionView + FetchedResultsController 有时会崩溃

UICollectionView + FetchResultsController sometimes crashes when i flush CoreData entity

我对通过 FetchResultsController 链接到 CoreData 实体的 UICollectionView 有疑问。我有一个显示搜索结果的屏幕,所以我需要从 API 获取记录,刷新核心数据实体并添加新记录。

它工作正常,但有时当我点击搜索按钮太快时,UICollection 会损坏并且不再更新。当我尝试使用不再存在的元素时发生错误是事实。

我添加了一些检查,但它在 100% 的情况下都没有帮助。

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


        blockOperations.append(BlockOperation(block: {
            if type == .insert {
                 self.ChatsCollectionView?.insertItems(at: [newIndexPath!])
            }

            if type == .delete {
                if ((indexPath?.row)! <= self.ChatsCollectionView.numberOfItems(inSection: 0) && self.ChatsCollectionView.numberOfItems(inSection: 0) != 0) {
                    self.ChatsCollectionView?.deleteItems(at: [indexPath!])
                }
            }

            if type == .update {
                if ((indexPath?.row)! <= self.ChatsCollectionView.numberOfItems(inSection: 0) && self.ChatsCollectionView.numberOfItems(inSection: 0) != 0) {
                    self.ChatsCollectionView?.reloadItems(at: [indexPath!])
                }
            }

            if type == .move {
                if let indexPath = indexPath {
                    self.ChatsCollectionView.deleteItems(at: [indexPath])
                }

                if let newIndexPath = newIndexPath {
                    self.ChatsCollectionView.insertItems(at: [newIndexPath])
                }
            }

        }))
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {

    ChatsCollectionView?.performBatchUpdates({

        for operation in self.blockOperations {
            operation.start()
        }

    }, completion: { (completed) in
        //self.ChatsCollectionView.reloadItems(at: self.ChatsCollectionView.indexPathsForSelectedItems!)
        // I comment this, because it cause crash 

    })
}

所以我现在没有崩溃,但有时我会出错并且集合看起来已损坏(有些行是空的等...):

我不使用 reloadData(),因为当我刷新实体中的数据时它会异步工作并崩溃。

对于 collectionView 和 tableView,如果你搞砸了并且你的数据源确实符合预期(即你插入了一行但行数没有增加),视图将停止进行任何更新并且可能会出现空行.

从 fetchedResultsController 进行更新比 Apple 文档中描述的更困难。您共享的代码会在同时移动和插入或移动和删除时导致此类错误。

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

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

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

如果你有部分,那么还要将部分更改保存在数组中,然后按顺序应用更改:删除(降序)、sectionDelete(降序)、sectionInserts(升序)、inserts(升序)、updates(任意顺序) ).部分无法移动或更新。

总结:

  1. 有 5 个数组:sectionInserts、sectionDeletes、rowDeletes、rowInserts 和 rowUpdates
  2. 在 controllerWillChangeContent 中清除所有数组
  3. in controller:didChangeObject: 将索引路径添加到数组中(移动是删除和插入)
  4. 在 controller:didChangeSection 中将节添加到 sectionInserts 或 rowDeletes 数组中
  5. 在controllerDidChangeContent中:处理如下:

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