Question

I've been working on this code all day, so it may be just something stupid that I am overlooking, but I can't seem to find out the problem and I'm at my wit's end.

-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    if(collectionView == self.titleBarCollectionView) {
        return 1;
    }
    NSInteger photoCount = [[self.fetchedResultsController fetchedObjects] count];
    NSInteger sectionCount = (photoCount + 3) / 3;
    return sectionCount;
}

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    if(collectionView != self.titleBarCollectionView) {
        id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
        NSInteger numberInSection = [sectionInfo numberOfObjects];
        if(numberInSection < 3) numberInSection++;
        return numberInSection;
    }
    return 3;
}

I have a UICollectionView which displays user's photos. The photos are taken from core data, however the first index (item 0, section 0) in the collection view is replaced with an "Add Photo" image and all of the photos indexPaths are adjusted to accommodate for this. It was working fine for hours, but for some reason after deleting the app (to reset core data), I cannot seem to even load the view. It instantly crashes with the error * Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array' when it reaches the line id sectionInfo = [self.fetchedResultsController sections][section];. I can't for the life of me figure it out. Here are the rest of my datasource methods.

-(void)configureCell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    if([cell.reuseIdentifier isEqualToString:@"Profile Title Cell"]) { 
        ... Irrelevant code ...
    } else if(indexPath.row == 0 && indexPath.section == 0) {
        UIImageView *imageView = (UIImageView *)[cell viewWithTag:300];
        imageView.image = [UIImage imageNamed:@"plusIcon.png"];
    } else {
        NSIndexPath *newIndexPath = [self newIndexPathFromIndexPath:indexPath];
        Photo *photo = [self.fetchedResultsController objectAtIndexPath:newIndexPath];
        UIImageView *imageView = (UIImageView *)[cell viewWithTag:300];

        NSData *imageData = [[NSData alloc] initWithBase64EncodedString:photo.thumbnailData options:0];
        imageView.image = [UIImage imageWithData:imageData];

        if(photo.imageData) {
            imageView.image = [UIImage imageWithData:photo.imageData];
        } else if(photo.imageURL && ![photo.imageURL isEqualToString:@""]) {
            NSString *imageURL = [NSString stringWithFormat:@"%@%@", PHOTOS_BASE_URL, photo.imageURL];
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
                self.activeThreads += 1;
                NSData *imageURLData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]];
                dispatch_async(dispatch_get_main_queue(), ^{
                    photo.imageData = imageURLData;
                    [self.imageCollectionView reloadItemsAtIndexPaths:@[indexPath]];
                    self.activeThreads -= 1;
                    if(self.activeThreads < 1)
                        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
                });
            });
        }
    }
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = nil;
    if(collectionView == self.titleBarCollectionView) {
        cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Profile Title Cell" forIndexPath:indexPath];
    } else {
        cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Profile Image Cell" forIndexPath:indexPath];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

- (NSIndexPath *)newIndexPathFromIndexPath:(NSIndexPath *)indexPath
{
    NSInteger row = 0;
    NSInteger section = 0;
    if(indexPath.row > 0) {
        row = indexPath.row - 1;
        section = indexPath.section;
    } else {
        row = 2;
        section = indexPath.section - 1;
    }
    return [NSIndexPath indexPathForItem:row inSection:section];
}

Edit: Here is my fetchedResultsController setter

- (NSFetchedResultsController *)fetchedResultsController
{
    if(!_fetchedResultsController) {
        NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Photo"];

        fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"uploadDate" ascending:YES]];

        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"ownerID == %@", self.currentUser.userID];

        _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                                        managedObjectContext:[RKManagedObjectStore defaultStore].mainQueueManagedObjectContext
                                                                          sectionNameKeyPath:@"sectionIdentifier"
                                                                                   cacheName:nil];

        _fetchedResultsController.delegate = self;

        [_fetchedResultsController performFetch:nil];
    }
    return _fetchedResultsController;
}
Was it helpful?

Solution

You said in a comment that fetchedObjects returns 8 objects. In numberOfSectionsInCollectionView you use this to calculate the number of sections in the collection view:

NSInteger photoCount = [[self.fetchedResultsController fetchedObjects] count];
NSInteger sectionCount = (photoCount + 3) / 3;
return sectionCount;

Assuming that the count is really 8, that means sectionCount is 3 (11 / 3, integer division). Note that you are not asking the fetched results controller how many sections it has, but you are telling the collection view that it should have three sections.

Since you return 3 above, collectionView:numberOfItemsInSection: will be called 3 times with section values of 0, 1, and 2. You pass the section value to the fetched results controller:

id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];

This only works if the fetched results controller actually has 3 sections. But you never asked it how many sections it has, so there's no guarantee that it has that many sections. In this situation a crash is extremely likely.

What it looks like happened is:

  • You didn't use sectionNameKeyPath when you created the fetched results controller
  • Therefore [self.fetchedResultsController sections] returns an empty array
  • But you try to look up an object in that empty array, because you assumed there would be 3 sections without checking whether that is true.

Therefore you crash. You can't just tell the fetched results controller how many sections it should have. The number of sections is based on the fetch results, and there aren't any sections unless you include a sectionNameKeyPath.

OTHER TIPS

It turns out collectionView:numberOfItemsInSection: was being called before the NSFetchedResultsController had executed performFetch: and therefore [self.fetchedResultsController sections] was empty. To fix this, I just set the number of items manually for each section with some simple logic.

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    NSArray *fetchedObjects = [self.fetchedResultsController fetchedObjects];
    NSInteger count = [fetchedObjects count] + 1;
    NSInteger sections = count / 3;
    if(sections > section) return 3;
    else return count % 3;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top