61

I m getting this error on performing insertItemsAtIndexPaths in UICollectionView

Assertion failure in:

-[UICollectionViewData indexPathForItemAtGlobalIndex:], 
/SourceCache/UIKit/UIKit-2372/UICollectionViewData.m:442
2012-09-26 18:12:34.432  
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', 
reason: 'request for index path for global index 805306367 
when there are only 1 items in the collection view'

I have checked and my datasource contains only one element. Any insights on why this could happen? If more information is needed I can definitely provide that.

Shmidt
  • 16,436
  • 18
  • 88
  • 136
jajo87
  • 1,388
  • 2
  • 11
  • 15

14 Answers14

68

I ran into this very same problem when inserting the first cell into a collection view. I fixed the problem by changing my code so that I call the UICollectionView

- (void)reloadData

method when inserting the first cell, but

- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths

when inserting all other cells.

Interestingly, I also had a problem with

- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths

when deleting the last cell. I did the same thing as before: just call reloadData when deleting the last cell.

Jay Slupesky
  • 1,893
  • 13
  • 14
  • 14
    This appears to be the correct answer. The crash occurs if you are inserting the first cell. There is an open radar on this problem: http://openradar.appspot.com/12954582 which says the issue also only occurs if you are using 'decorated views' (section headers or footers). – chris Jan 22 '13 at 16:06
  • 2
    Supplementary views and using reloadData on first item and insertItemsAtIndexPaths on following items, doesn't work for me. However if I disable supplementary views then it works. And if I only use reloadData it works. Sadly I would like to use -insertItemsAtIndexPaths with my KVO code. – neoneye Jan 24 '13 at 20:56
  • to expand on this answer, if I call `reloadData` outside of and prior to the `performBatchUpdates:` block, the UI updates as desired and the crash goes away. – toblerpwn Apr 16 '13 at 06:56
  • @neoneye Curious if this is still a problem for you? I haven't had any trouble with batch inserts and supplementary views co-existing. – Timothy Moose Jun 20 '13 at 23:05
  • @TimothyMoose I don't have time to check this. I hope it's fixed in iOS7. – neoneye Jun 22 '13 at 20:33
  • 2
    In iOS7 its always crashing for me unless I use reloadData for cell insertion AND deletion!!! I have a complex headerView, and was testing with simple cells - which worked just fine. When the cells got more complex, then the crashes began... – David H Nov 05 '13 at 15:35
  • 1
    Doesn't work for me on iOS 7.1. I don't have any decoration views and this error occurs at varying cell numbers, not just when adding the first cell. – Ortwin Gentz May 08 '14 at 22:31
11

Inserting section#0 just before inserting cells seems make UICollectionView happy.

NSArray *indexPaths = /* indexPaths of the cells to be inserted */
NSUInteger countBeforeInsert = _cells.count;
dispatch_block_t updates = ^{
    if (countBeforeInsert < 1) {
        [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:0]];
    }
    [self.collectionView insertItemsAtIndexPaths:indexPaths];
};
[self.collectionView performBatchUpdates:updates completion:nil];
neoneye
  • 50,398
  • 25
  • 166
  • 151
  • This seems to work for me, I'm also using a supplementary view for the section – Adam Carter Apr 04 '13 at 11:04
  • 2
    In order to get this to work I had to tweak the return value of the `numberOfSectionsInCollectionView:` method. Return 0 sections if your collection is empty, and return 1 if the collection is not. If you are not implementing this method or returning 0, you will get 'invalid update' errors complaining about the number of sections not being right after you do the insert. – nrj Aug 29 '13 at 18:09
  • It works for me. I got the same error when I try to insert the firs cell of a section. Now I changed to: 1, for insert the first cell of a section, use insertSections: instead of insertItemsAtIndexPaths:. 2, for the reset cells of an existing section, use insertItemsAtIndexPaths:. – Lei Zhao Dec 16 '14 at 00:19
5

I've posted a work around for this issue here: https://gist.github.com/iwasrobbed/5528897

In the private category at the top of your .m file:

@interface MyViewController ()
{
    BOOL shouldReloadCollectionView;
    NSBlockOperation *blockOperation;
}
@end

Then your delegate callbacks would be:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    shouldReloadCollectionView = NO;
    blockOperation = [NSBlockOperation new];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    __weak UICollectionView *collectionView = self.collectionView;
    switch (type) {
        case NSFetchedResultsChangeInsert: {
            [blockOperation addExecutionBlock:^{
                [collectionView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
            }];
            break;
        }

        case NSFetchedResultsChangeDelete: {
            [blockOperation addExecutionBlock:^{
                [collectionView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
            }];
            break;
        }

        case NSFetchedResultsChangeUpdate: {
            [blockOperation addExecutionBlock:^{
                [collectionView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
            }];
            break;
        }

        default:
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    __weak UICollectionView *collectionView = self.collectionView;
    switch (type) {
        case NSFetchedResultsChangeInsert: {
            if ([self.collectionView numberOfSections] > 0) {
                if ([self.collectionView numberOfItemsInSection:indexPath.section] == 0) {
                    shouldReloadCollectionView = YES;
                } else {
                    [blockOperation addExecutionBlock:^{
                        [collectionView insertItemsAtIndexPaths:@[newIndexPath]];
                    }];
                }
            } else {
                shouldReloadCollectionView = YES;
            }
            break;
        }

        case NSFetchedResultsChangeDelete: {
            if ([self.collectionView numberOfItemsInSection:indexPath.section] == 1) {
                shouldReloadCollectionView = YES;
            } else {
                [blockOperation addExecutionBlock:^{
                    [collectionView deleteItemsAtIndexPaths:@[indexPath]];
                }];
            }
            break;
        }

        case NSFetchedResultsChangeUpdate: {
            [blockOperation addExecutionBlock:^{
                [collectionView reloadItemsAtIndexPaths:@[indexPath]];
            }];
            break;
        }

        case NSFetchedResultsChangeMove: {
            [blockOperation addExecutionBlock:^{
                [collectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
            }];
            break;
        }

        default:
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    // Checks if we should reload the collection view to fix a bug @ http://openradar.appspot.com/12954582
    if (shouldReloadCollectionView) {
        [self.collectionView reloadData];
    } else {
        [self.collectionView performBatchUpdates:^{
            [blockOperation start];
        } completion:nil];
    }
}

Credit for this approach goes to Blake Watters.

iwasrobbed
  • 46,496
  • 21
  • 150
  • 195
  • 1
    I had found Blake's approach here: https://gist.github.com/Lucien/4440c1cba83318e276bb but I could not get it to work due to the bug thing (I think). So thanks a lot for taking the time to post this full version. It works very well! – PJC Oct 08 '13 at 16:57
  • Thanks. Looks like the if clause in `NSFetchedResultsChangeInsert` should check for the number of items in section `*new*IndexPath.section`, though. – Simon Whitaker Nov 08 '13 at 11:21
  • Using NSBlockOperation like this is dangerous, since it doesn't guarantee that the added blocks will be called on the main thread. See my comment [here](https://gist.github.com/Lucien/4440c1cba83318e276bb#comment-1184323) for details. – Matej Bukovinski Mar 04 '14 at 14:44
4

Here is a non-hack, docs-based answer to the problem. In my case, there was a condition according to which I would return a valid or a nil supplementary view from collectionView:viewForSupplementaryElementOfKind:atIndexPath:. After encountering the crash, I checked the docs and here is what they say:

This method must always return a valid view object. If you do not want a supplementary view in a particular case, your layout object should not create the attributes for that view. Alternatively, you can hide views by setting the hidden property of the corresponding attributes to YES or set the alpha property of the attributes to 0. To hide header and footer views in a flow layout, you can also set the width and height of those views to 0.

There are other ways to do this, but the quickest seems to be:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewFlowLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
    return <condition> ? collectionViewLayout.headerReferenceSize : CGSizeZero;
}
Victor Bogdan
  • 2,022
  • 24
  • 33
3

My collection view was getting items from two data sources and updating them caused this issue. My workaround was to queue the data update and collection view reload together:

[[NSOperationQueue mainQueue] addOperationWithBlock:^{

                //Update Data Array
                weakSelf.dataProfile = [results lastObject]; 

                //Reload CollectionView
                [weakSelf.collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:0]]];
 }];
Alex L
  • 8,419
  • 6
  • 43
  • 51
1

Check that you are returning the correct number of elements in the UICollectionViewDataSource methods:

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

and

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
Morrowless
  • 6,856
  • 11
  • 51
  • 81
  • Yup its returning just 1. Apparently this issue happens only on inserting the first item. And is not an issue from then on. But instead, when I tried just "reloadData" collection view on the first item there is no issue. – jajo87 Sep 27 '12 at 15:56
1

I ran into this problem as well. Here's what happened to me:

  1. I subclassed UICollectionViewController and, on initWithCollectionViewLayout:, was initializing my NSFetchedResultsController.
  2. Have a shared class fetch results from an NSURLConnection and parse the JSON string (different thread)
  3. Loop through the feed and create my NSManagedObjects, add them to my NSManagedObjectContext, which has the main thread's NSManagedObjectContext as a parentContext.
  4. Save my context.
  5. Have my NSFetchedResultsController pick up the changes and queue them up.
  6. On - (void)controllerDidChangeContent:, I would process the changes and apply them to my UICollectionView.

Intermittently, I would get the error the OP is getting and couldn't figure out why.

To fix this issue, I moved the NSFetchedResultsController initialization and performFetch to my - viewDidLoad method and this problem is now gone. No need to call [collectionView reloadData] or anything and all the animations are working properly.

Hope this helps!

Simon Germain
  • 6,834
  • 1
  • 27
  • 42
1

It seems that the problem occurs when you either insert or move a cell to a section that contains a supplementary header or footer view (with UICollectionViewFlowLayout or a layout derived from that) and the section has a count of 0 cells before the insertion / move.

I could only circumvent the crash and still maintain the animations by having an empty and invisible cell in the section containing the supplementary header view like this:

  1. Make - (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section return the actual cell `count + 1 for that section where the header view is.
  2. In - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath return

    if ((indexPath.section == YOUR_SECTION_WITH_THE_HEADER_VIEW) && (indexPath.item == [self collectionView:collectionView numberOfItemsInSection:indexPath.section] - 1)) {
            cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"EmptyCell" forIndexPath:indexPath];
    }
    

    ... an empty cell for that position. Remember to register the cell reuse in viewDidLoad or wherever you initialize your UICollectionView:

    [self.collectionView registerClass:[UICollectionReusableView class] forCellWithReuseIdentifier:@"EmptyCell"];
    
  3. Use moveItemAtIndexPath: or insertItemsAtIndexPaths: without crashing.
Markus Rautopuro
  • 7,997
  • 6
  • 47
  • 60
1

Here's a solution for this bug I've been using in my projects I thought I'd post here in case any found it valuable.

@interface FetchedResultsViewController ()

@property (nonatomic) NSMutableIndexSet *sectionsToAdd;
@property (nonatomic) NSMutableIndexSet *sectionsToDelete;

@property (nonatomic) NSMutableArray *indexPathsToAdd;
@property (nonatomic) NSMutableArray *indexPathsToDelete;
@property (nonatomic) NSMutableArray *indexPathsToUpdate;

@end
#pragma mark - NSFetchedResultsControllerDelegate


- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self resetFetchedResultControllerChanges];
}


- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            [self.sectionsToAdd addIndex:sectionIndex];
            break;

        case NSFetchedResultsChangeDelete:
            [self.sectionsToDelete addIndex:sectionIndex];
            break;
    }
}


- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            [self.indexPathsToAdd addObject:newIndexPath];
            break;

        case NSFetchedResultsChangeDelete:
            [self.indexPathsToDelete addObject:indexPath];
            break;

        case NSFetchedResultsChangeUpdate:
            [self.indexPathsToUpdate addObject:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [self.indexPathsToAdd addObject:newIndexPath];
            [self.indexPathsToDelete addObject:indexPath];
            break;
    }
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if (self.sectionsToAdd.count > 0 || self.sectionsToDelete.count > 0 || self.indexPathsToAdd.count > 0 || self.indexPathsToDelete > 0 || self.indexPathsToUpdate > 0)
    {
        if ([self shouldReloadCollectionViewFromChangedContent])
        {
            [self.collectionView reloadData];

            [self resetFetchedResultControllerChanges];
        }
        else
        {
            [self.collectionView performBatchUpdates:^{

                if (self.sectionsToAdd.count > 0)
                {
                    [self.collectionView insertSections:self.sectionsToAdd];
                }

                if (self.sectionsToDelete.count > 0)
                {
                    [self.collectionView deleteSections:self.sectionsToDelete];
                }

                if (self.indexPathsToAdd.count > 0)
                {
                    [self.collectionView insertItemsAtIndexPaths:self.indexPathsToAdd];
                }

                if (self.indexPathsToDelete.count > 0)
                {
                    [self.collectionView deleteItemsAtIndexPaths:self.indexPathsToDelete];
                }

                for (NSIndexPath *indexPath in self.indexPathsToUpdate)
                {
                    [self configureCell:[self.collectionView cellForItemAtIndexPath:indexPath]
                            atIndexPath:indexPath];
                }

            } completion:^(BOOL finished) {
                [self resetFetchedResultControllerChanges];
            }];
        }
    }
}

// This is to prevent a bug in UICollectionView from occurring.
// The bug presents itself when inserting the first object or deleting the last object in a collection view.
// http://stackoverflow.com/questions/12611292/uicollectionview-assertion-failure
// This code should be removed once the bug has been fixed, it is tracked in OpenRadar
// http://openradar.appspot.com/12954582
- (BOOL)shouldReloadCollectionViewFromChangedContent
{
    NSInteger totalNumberOfIndexPaths = 0;
    for (NSInteger i = 0; i < self.collectionView.numberOfSections; i++)
    {
        totalNumberOfIndexPaths += [self.collectionView numberOfItemsInSection:i];
    }

    NSInteger numberOfItemsAfterUpdates = totalNumberOfIndexPaths;
    numberOfItemsAfterUpdates += self.indexPathsToAdd.count;
    numberOfItemsAfterUpdates -= self.indexPathsToDelete.count;

    BOOL shouldReload = NO;
    if (numberOfItemsAfterUpdates == 0 && totalNumberOfIndexPaths == 1)
    {
        shouldReload = YES;
    }

    if (numberOfItemsAfterUpdates == 1 && totalNumberOfIndexPaths == 0)
    {
        shouldReload = YES;
    }

    return shouldReload;
}

- (void)resetFetchedResultControllerChanges
{
    [self.sectionsToAdd removeAllIndexes];
    [self.sectionsToDelete removeAllIndexes];
    [self.indexPathsToAdd removeAllObjects];
    [self.indexPathsToDelete removeAllObjects];
    [self.indexPathsToUpdate removeAllObjects];
}
gdavis
  • 2,556
  • 1
  • 20
  • 25
1

In my case, the problem was the way I was creating my NSIndexPath. For example, to delete the 3rd cell, instead of doing :

NSIndexPath* indexPath = [NSIndexPath indexPathWithIndex:2];
[_collectionView deleteItemsAtIndexPaths:@[indexPath]];

I needed to do :

NSIndexPath* indexPath = [NSIndexPath indexPathForItem:2 inSection:0];
[_collectionView deleteItemsAtIndexPaths:@[indexPath]];
Raphael Royer-Rivard
  • 2,252
  • 1
  • 30
  • 53
1

Check that you're returning the correct value in numberOfSectionsInCollectionView:

The value I was using to calculate sections was nil, thus 0 sections. This caused the exception.

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    NSInteger sectionCount = self.objectThatShouldHaveAValueButIsActuallyNil.sectionCount;

    // section count is wrong!

    return sectionCount;
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
0

Just for the record, I ran into the same problem and for me the solution was to remove the header (disable them in the .xib) and as them were not needed anymore removed this method. After that everything seems fine.

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementary
ElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
PakitoV
  • 2,476
  • 25
  • 34
0

I hit this problem myself. All the answers here seemed to present problems, except for Alex L's. Referring the update seemed to be tha answer. Here is my final solution:

- (void)addItem:(id)item {
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        if (!_data) {
            _data = [NSMutableArray arrayWithObject:item];
        } else {
            [_data addObject:item];
        }
        [_collectionView insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:_data.count-1 inSection:0]]];
    }];
}
Owen Godfrey
  • 3,361
  • 2
  • 22
  • 18
0

The workaround that actually works is to return a height of 0 if the cell at your supplementary view's index path is not there (initial load, you've deleted the row, etc). See my answer here:

https://stackoverflow.com/a/18411860/917104

Community
  • 1
  • 1
deepseadiving
  • 763
  • 5
  • 18