9

I've got a typical UICollectionView which is using UICollectionViewFlowLayout in a vertical fashion. I'm using a rest API with pagination to populate the collection view. In order to trigger the next page to download, I'm using the delegate when it asks for the footer layout:

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath{
    RDLoadMoreReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@"RDLoadMoreReusableView" forIndexPath:indexPath];
    view.delegate = self;
    [view start];
    [self loadNextPageOfAssets];
    return view;
}

Here is the code behind my loadNextPageOfAssets:

-(void)loadNextPageOfAssets{
    __weak RDRadiusGridViewController *weakSelf = self;

    // Increment page and request assets. They are added to cluster.assets before the completion block is called.
    self.cluster.pagination.page++;
    [self.cluster requestAssetsWithCompletionBlock:^(NSArray *assets) {

        SM_LOG_DEBUG(@"page.total: %ld assets.count: %ld", self.cluster.pagination.totalCount, (long)assets.count);

        NSMutableArray *indexPaths = [[NSMutableArray alloc]initWithCapacity:assets.count];
        for(NSUInteger index = 0; index < assets.count; index++){
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.cluster.assets.count - assets.count + index inSection:0];
            SM_LOG_DEBUG(@"Inserting indexPath: %ld:%ld", (long)indexPath.item, (long)indexPath.section);
            [indexPaths addObject:indexPath];
        }
        [weakSelf.collectionView performBatchUpdates:^{
            [weakSelf.collectionView insertItemsAtIndexPaths:indexPaths];
        } completion:^(BOOL finished) {

        }];

//            [weakSelf.collectionView reloadData];
    }];
}

When I run I can go to my ViewController and see the first page of assets loaded. If I scroll down I'll see the footer view (which contains a spinner), but then the code will break at the exception break point at line:

[weakSelf.collectionView insertItemsAtIndexPaths:indexPaths];

Assertion failure in -[UICollectionViewData layoutAttributesForSupplementaryElementOfKind:atIndexPath:], /SourceCache/UIKit/UIKit-3185.20/UICollectionViewData.m:829


Then if I continue it crashes with the error:

2014-06-11 16:39:58.335 Radius-iOS[4901:525006] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no UICollectionViewLayoutAttributes instance for -layoutAttributesForSupplementaryElementOfKind: UICollectionElementKindSectionFooter at path {length = 2, path = 0 - 0}' * First throw call stack:


This is puzzling to me. layoutAttributesForSupplementaryElementOfKind sounds like it's seomthing to do with the layout class, but I'm not using a custom flow layout rather the default one supplied. It sounds like Apple's code is mad but I feel that I'm using everything correctly.

Now if I move the call:

[self loadNextPageOfAssets];

from the supplementary cell dequeue to teh UICollectionViewCell deque, and remove the footer views all together, then the insert works great.

For now I'm calling reloadData instead of insert, but this is UGLY.

Am I overlooking something about the footers?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
VaporwareWolf
  • 10,143
  • 10
  • 54
  • 80
  • wel.. for starters you could check what is the supplementary element kind that is passed into viewForSupplementaryElementOfKind – govi Jun 19 '14 at 16:50

4 Answers4

10

The answer provided by VaporwareWolf is a bit of a hack. By returning a tiny size instead of a zero size, the supplementary view will always exist but at a size too small to see. So that's why it fixes the NSInternalInconsistencyException

But, there is a real solution.

After adding the data to the datasource and before calling insertItemsAtIndexPaths, just invalidate the layout on the collectionview to make it aware of the changes.

Objective-C

[self.collectionView.collectionViewLayout invalidateLayout]

Swift

collectionView.collectionViewLayout.invalidateLayout()
Johannes
  • 1,370
  • 16
  • 15
  • I'll agree with you. I have learned a lot about UICollectionView since I made this post years ago. Changing this to accepted answer. – VaporwareWolf Apr 05 '16 at 21:30
  • Thanks for the great answer. I am also facing the same problem. Using the default custom layout and return CGSizeZero for my footer view when data source is empty. The app will crash if I don't call invalidateLayout before inserting or moving methods – PrimaryChicken Jul 25 '17 at 05:56
9

It turns out that I was actually experiencing a problem with layout (as the error description suggests)

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section{
    if(<myDecision>){
        return CGSizeZero;
    } else {
        return CGSizeMake(320, 50);
    }
}

CGSizeZero is what was causing the crash. Instead use CGSizeMake(0.001, 0.001). That's the only change that was necessary. It runs as intended now.

VaporwareWolf
  • 10,143
  • 10
  • 54
  • 80
  • 1
    Thanks for this! Any reason why one can't simply return CGSizeZero? It's extremely annoying to have to handle collection view's tantrums :( – batman Jun 11 '15 at 05:30
  • that was super frustrating but this sorted it thanks! :) – agandi Dec 08 '17 at 17:35
0

There is a huge article in Russian about bugs in UICollectionView: http://habrahabr.ru/post/211144/.

Here are a couple of methods from that article that may solve your problem:

@try {
    [self.collectionView insertItemsAtIndexPaths:indexPaths];
}
@catch (NSException *exception) {}

or

[self.collectionView performBatchUpdates:^{
        [self.collectionView reloadData];
    } completion:nil];
bzz
  • 5,556
  • 24
  • 26
0

You should implement both (Header and Footer) methods. Even if you want only one. Look at my code

-(UICollectionReusableView *) collectionView:(UICollectionView *) collectionView
           viewForSupplementaryElementOfKind:(NSString *) kind
                                 atIndexPath:(NSIndexPath *) indexPath
{
    UICollectionReusableView *header = [[UICollectionReusableView alloc] init];

    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {

        header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"CollectionViewHeader" forIndexPath:indexPath];
    }
    else {
        header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"CollectionViewHeader" forIndexPath:indexPath];
    }

    return header;
}

-(CGSize) collectionView:(UICollectionView *) collectionView
                         layout:(UICollectionViewLayout *) collectionViewLayout
referenceSizeForHeaderInSection:(NSInteger) section
{
    return CGSizeMake(self.view.frame.size.width, 100);
}

-(CGSize) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
    return CGSizeZero;
}
Ivan Vavilov
  • 1,520
  • 1
  • 15
  • 28