swiftcore-datansfetchedresultscontrollerflush

UICollectionView + FetchResultsController sometimes crashes when i flush CoreData entity


I have an issue with UICollectionView that linked to CoreData Entity via FetchResultsController. I have a screen with search results, so i need to get records from API, flush Core Data Entity and add new records.

It works fine, but sometimes when i click search button too fast UICollection becomes broken and does not updates anymore. It's a fact that error occurred when i am trying to work with elements that does not exists anymore.

I've added some checks, but it not helps in 100% of situations.

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 

    })
}

So i have no crashes now, but sometimes i get errors and Collection looks damaged (some rows are emty etc...):

I don't use reloadData() because it works async and crash when i flush data in Entity.


Solution

  • With collectionView and tableView if you ever mess up and your datasource does match what is expected (ie you inserted a row but the number of row did not increase) the view will stop doing any updates and can appear with empty rows.

    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.

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

    For updates you don't care where it was BEFORE the inserts and delete - only after - so use newIndexPath not indexPath. This will fix crashes 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.

    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 (you can use a custom object or dictionary - whatever works for you). 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.

    If you have section 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.

    Summary:

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

    2. in controllerWillChangeContent clear all the arrays

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

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

    5. in controllerDidChangeContent: process them as follows:

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