Search code examples
iosswiftuicollectionviewrealm

How to prevent NSInternalInconsistencyException with Realm and UICollectionView


I have a UICollectionView that is populated from Realm. Some users, seemingly at random, get a NSInternalInconsistencyException stating something like

Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (73) must be equal to the number of items contained in that section before the update (73), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).

My code is based on Realm's collection example. It selects and filters some records:

self.assets = realm.objects(Asset.self).filter("is_deleted = false")

And then it subscribes to and handles notifications:

self.assetsNotificationToken = self.assets!.addNotificationBlock(){ [weak self] (changes: RealmCollectionChange) in

    guard let collectionView = self!.collectionView else { return }

    guard let strongSelf = self else { return }

    switch changes {

        case .Initial:

            collectionView.reloadData()

        case .Update(let _, let deletions, let insertions, let modifications):

            strongSelf.collectionView?.performBatchUpdates({

                collectionView.insertItemsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) })

                collectionView.reloadItemsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) })

                collectionView.deleteItemsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) })

            }, completion: nil)

        case .Error(let error):

            log.error(error.localizedDescription)
            break

    }

}

The count comes from:

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

    guard assets != nil else {
        return 0;
    }

    return assets!.count

}

I later switched to RealmGridController

Unable to find the source of the crash, I switched to RealmGridController. It's a package written by a Realm core contributor and it encapsulates all of the standard functionality needed for working with realm + CollectionViews.

It seemed to work, and then I started seeing the exact same crash.

Fatal Exception: NSInternalInconsistencyException Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (78) must be equal to the number of items contained in that section before the update (78), plus or minus the number of items inserted or deleted from that section (2 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).

Fatal Exception: NSInternalInconsistencyException
0  CoreFoundation                 0x1839dadb0 __exceptionPreprocess
1  libobjc.A.dylib                0x18303ff80 objc_exception_throw
2  CoreFoundation                 0x1839dac80 +[NSException raise:format:]
3  Foundation                     0x184360154 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:]
4  UIKit                          0x18938b00c -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:]
5  UIKit                          0x18938e464 -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:]
6  UIKit                          0x18938e2e0 -[UICollectionView _performBatchUpdates:completion:invalidationContext:]
7  UIKit                          0x188d2c2a4 -[UICollectionView performBatchUpdates:completion:]
8  RealmGridController            0x1014a8340 specialized RealmGridController.controllerDidChangeContent(RBQFetchedResultsController) -> () (RealmGridController.swift:316)
9  RealmGridController            0x1014a687c @objc RealmGridController.controllerDidChangeContent(RBQFetchedResultsController) -> () (RealmGridController.swift)
10 RBQFetchedResultsController    0x100ff8edc __112-[RBQFetchedResultsController calculateChangesWithAddedSafeObjects:deletedSafeObjects:changedSafeObjects:realm:]_block_invoke.433 (RBQFetchedResultsController.m:842)
11 libdispatch.dylib              0x1834254bc _dispatch_call_block_and_release

Solution

  • RealmGridController is a library that was written before Realm supported fine-grained notifications and so it implements a lot of custom logic to get the same results. I'd strongly recommend you revert back to the original logic you had as that one is deeply integrated into Realm itself.

    One important thing to remember is that Realm query results are live objects; they automatically update on each run loop iteration to include any changes that have since happened to it. As a result, the contents of self.assets should always be a direct 1-to-1 correlation of the content being displayed by your collection view. The fine-grained notifications notification only serves as a mechanism to update any outdated UI elements, and by the time it is called, self.assets will already be in the up-to-date state.

    You may need to provide some more information on how your app modifies the contents of self.assets as it runs. How are elements added and deleted? Is any concurrency occurring in your app here?

    You need to be very careful not to do any manual UI updates, as any unexpected divergences from the collection view's previous state and the what Realm is assuming it needs to be updated will generate these inconsistency exceptions.