28

I am using performBatchUpdates() to update my collection view, where I am doing a complete refresh, i.e. delete whatever was in it and re-insert everything. The batch updates are done as part of an Observer which is attached to a NSMutableArray (bingDataItems).

cellItems is the array containing items that are or will be inserted into the collection view.

Here is the code:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    cultARunner *_cultARunner = [cultARunner getInstance];
    if ( [[_cultARunner bingDataItems] count] ) {
        [self.collectionView reloadData];
        [[self collectionView] performBatchUpdates: ^{

            int itemSize = [cellItems count];
            NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];

            // first delete the old stuff
            if (itemSize == 0) {
                [arrayWithIndexPaths addObject: [NSIndexPath indexPathForRow: 0 inSection: 0]];
            }
            else {
                for( int i = 0; i < cellItems.count; i++ ) {
                    [arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
                }
            }
            [cellItems removeAllObjects];
            if(itemSize) {
                [self.collectionView deleteItemsAtIndexPaths:arrayWithIndexPaths];
            }

            // insert the new stuff
            arrayWithIndexPaths = [NSMutableArray array];
            cellItems = [_cultARunner bingDataItems];
            if ([cellItems count] == 0) {
                [arrayWithIndexPaths addObject: [NSIndexPath indexPathForRow: 0 inSection: 0]];
            }
            else {              
                for( int i = 0; i < [cellItems count]; i++ ) {
                    [arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
                }
            }
            [self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
        }
        completion:nil];
    }
}

I get this error, but not all of the times (why ?)

2012-12-16 13:17:59.789 [16807:19703] *** Assertion failure in -[UICollectionViewData indexPathForItemAtGlobalIndex:], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionViewData.m:442
2012-12-16 13:17:59.790 [16807:19703] DEBUG:    request for index path for global index 1342177227 when there are only 53 items in the collection view

I checked the only thread that mentioned the same problem here: UICollectionView Assertion failure, but it is not very clear i.e. doing [collectionview reloadData] is not advisable in the performBatchUpdates() block.

Any suggestions on what might be going wrong here ?

Community
  • 1
  • 1
DancingJohn
  • 557
  • 1
  • 9
  • 17
  • Here's another cause and solution: https://fangpenlin.com/posts/2016/04/29/uicollectionview-invalid-number-of-items-crash-issue/ – abc123 Mar 04 '22 at 21:52

8 Answers8

20

Finally! Ok, here's what was causing this crash for me.

As previously noted, I was creating supplementary views in order to provide custom-styled section headers for my collection view.

The problem is this: it appears that the indexPath of a supplementary view MUST correspond to the indexPath of an extant cell in the collection. If the supplementary view's index path has no corresponding ordinary cell, the application will crash. I believe that the collection view attempts to retrieve information for a supplementary view's cell for some reason during the update procedure. It crashes when it cannot find one.

Hopefully this will solve your problem too!

Ash
  • 9,064
  • 3
  • 48
  • 59
  • 2
    Excellent. This is the right answer! Item-less sections are crashy. – Felipe Baytelman May 10 '13 at 16:30
  • Ash, thank You, I also have this kind of crashes in my application, you really helped. – Alexander Tkachenko Nov 20 '13 at 10:00
  • Great answer. It's obnoxious because you can't "move" an item out of a section that you are deleting in the same `performBatchUpdates` .. so if you move all the items out of a section, the only valid way to prevent this error from what I found is to add a temporary dummy invisible cell to the section as I move the items, and then delete the section on the completion of the `performBatchUpdates` .. although I may be missing something – chasew Apr 15 '14 at 18:34
  • Sorry but I did not understand what is the solution. Should I not use the .item in my cellforindex ? – PoolHallJunkie Aug 31 '16 at 09:34
  • Not sure how much clearer I can put it, really. The long and short of it is, if you pass an index path for a supplementary view and there isn't also a regular item cell corresponding to that index path, you'll get this error. – Ash Sep 01 '16 at 11:50
  • This is not true. If there is a supplementary view attributes that goes beyond the given number of sections, an exception is thrown, but as long as the number of sections match, the number of items don't matter. Test it for yourself. Just for testing purposes, I just made a custom layout with two supplementary views and one cell. It works fine. – funct7 Apr 26 '17 at 02:22
  • Note that the question is four years old, and collection views received a sizeable overhaul in a recent iOS update. I'm fairly certain that it was true at time of writing as I remember doing extensive testing myself, however it's not unlikely that it has been changed by Apple since then. – Ash Apr 26 '17 at 06:46
17

This is the proper workaround to this crash:

Each of your supplementary views are associated with a certain index path. If you don't have a cell at that index path (initial load, you've deleted the row, etc), return a height of 0 for your supplementary view via your layout's delegate.

So, for a flow layout, implement UICollectionViewDelegateFlowLayout's

(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section

method (and the corresponding footer method, if you're using footers) with the following logic

if ( you-have-a-cell-at-the-row-for-this-section )
    return myNormalHeaderSize;
else return CGSizeMake( 0,0 );

Hope this helps!

deepseadiving
  • 763
  • 5
  • 18
  • I love it when a question is live for 9 months and I find it 4 days after the right solution is finally found! – cyphers Aug 27 '13 at 07:03
  • 2
    What if I still want the section header visible even though the section is empty? – onlygo Aug 27 '13 at 23:09
  • @SunJian the workaround is that the cell associated with your header has to exist, so you'd have to have a cell that's always around in order to have your section header visible. – deepseadiving Aug 28 '13 at 15:20
  • @SunJian it's also worth noting that you shouldn't be thinking of this as a section header. It's not, it's a supplementary view; supplementary to a specific cell that needs to exist. So if that cell isn't around, your supplementary view has to be nil or have a height of 0. – deepseadiving Aug 28 '13 at 15:27
8

reloadData doesn't work for me, because the whole purpose of using performBatchUpdates is to get the changes animated. If you use reloadData you only refresh the data, but without animations.

So suggestions of "replace performBatchUpdates with reloadData" is pretty much saying "give up on what you're trying to do."

I'm sorry, I'm just frustrated because this error keeps coming up for me while I'm trying to do some great animated updates and my model is 100 % correct, it's some iOS magic inside getting broken and forcing me to change my solutions completely.

My opinion is that Collection Views are still buggy and can't do complicated animated refreshes, even though they should be able to. Because this used to be the same thing for Table Views but those are now pretty stable (it took time, though).

//Edit (Sep 1, 2013) The reported bug is closed now so this issues seems to have been resolved by Apple already.

czechboy
  • 899
  • 7
  • 15
  • 1
    i suggest making a simple example and submitting it as a bug to Apple. – bshirley Feb 05 '13 at 17:36
  • 2
    I did that 4 weeks ago, they marked it as duplicate and the original bug is still open. So that doesn't look good. – czechboy Feb 11 '13 at 08:40
  • 1
    I've discovered one way to hack around the problem that will keep you in development while a permanent solution is in the works. Make a custom layout object if you haven't already, override prepareForCollectionViewUpdates: and don't call [super prepareForCollectionViewUpdates:]. This is likely to have knock-on effects elsewhere and is NOT a permanent solution, but it might make the darn thing work at least. – Ash Mar 02 '13 at 18:41
  • Sounds good, but who knows what else is going on in prepareForCollectionViewUpdates:. But thanks for the suggestion, I'll definitely try it. – czechboy Mar 08 '13 at 09:12
  • I'd be more interested to know what's *supposed* to be going on in the overridden version! For the record, I believe the crash is happening for me because the edited items' indexPath.section is being given as NSNotFound. – Ash Mar 16 '13 at 20:19
  • 3
    Thanks, Ash. I've essentially used your solution (overriding prepareForCollectionViewUpdates: but rather than never calling super. I'm just wrapping it in a try/catch. In the little bit of testing I've done, that seems to be working. Wrapping it in a @try{}@catch{} has the added benefit that when the bug is fixed I won't have to change anything. – Derrick Hathaway Mar 28 '13 at 03:27
  • 1
    Even in iOS 7, this seems to still be a problem. Thanks for the ideas, Ash & Derrick! BTW, if you do use the try-catch approach, remember to use `-fobjc-arc-exception` for that file. – Jonathan Sterling Jan 17 '14 at 20:19
  • So annoying. I remember first finding this bug 18 months ago in iOS 6 beta and they still haven't fixed it. They better do something about it, because collection views with this crashing are just unusable. I prefer hacking away with good old table views now. – czechboy Jan 23 '14 at 23:49
3

I have been having the same problem.

I have tried a number of variations, but the final one that seems to work is [self.collectionView reloadData], where "self.collectionView"is the name of your collection view.

I have tried the following methods, straight from the "UICollectionView Class Reference": inserting, moving, and deleting items.

These were used at first, to "move" the item from one section to another.

  • deleteItemsAtIndexPaths:

  • insertItemsAtIndexPaths:

Next, I tried moveItemAtIndexPath:toIndexPath:.

They all produced the following error:

Assertion failure in -[UICollectionViewData indexPathForItemAtGlobalIndex:], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionViewData.m:442

So, try the "reloadData" method.

apaderno
  • 28,547
  • 16
  • 75
  • 90
Jim
  • 31
  • 1
2

If you remove the last cell from a section containing header/footer the bug appears.

I tried to return nil for header/footer size/element at that time and this sometimes fixes the issue.

Options:

  1. Reload the whole table view instead of animating the removal of the last item.
  2. Add an additional invisible, basic cell with a size less than 1.
HeikoG
  • 861
  • 10
  • 11
2

A cheeseball mistake that can lead to this error is reusing the same UICollectionViewFlowLayout on multiple collectionViews on the same viewcontroller! Just init different flowLayouts for each collectionview and you'll be good to go!

Chris Klingler
  • 5,258
  • 2
  • 37
  • 43
  • 1
    Thank god I found your hint to not share the same layout for multiple collection views. I was about to pull my hair out – Stefan Arn Apr 07 '16 at 14:57
1

I ran into this problem when I delete one of the cells from my collection view.

The problem was that I use a custom layout, and the call layoutAttributesForElementsInRect was returning more than the number of cells in the collection view after the delete.

Apparently UICollectionView just iterates through the array returned by the method without checking the number of cells.

Modifying the method to return the same number of layout attributes solved the crash.

Allen Zeng
  • 2,635
  • 2
  • 20
  • 31
0

I still couldn't figure out how the global index was incremented so much, but I solved my problem by inserting a temporary item in the underlying datasource array i.e. cellItems and calling [self.collectionview reloadData] in viewDidLoad().

This inserts a placeholder cell temporarily in the collection view until I trigger the actual process using performBatchUpdates().

DancingJohn
  • 557
  • 1
  • 9
  • 17