Search code examples
ioscore-datauicollectionviewnsfetchedresultscontrollernsrangeexception

Accessing database using sections in UICollection


I am developing small application in iOS (I am complete beginner). I have made a menu Core Data entity where I have categoryName and imageNamed saved. I want my UICollectionView in sections. But I am stuck in defining different indexPaths for different sections.

I also feel that my way of accessing data is very poor and seems inefficient as I am defining two NSFetchedResultsController instances. How can I access the data store better?

I get this error:

14-03-17 10:02:19.974 citiliving[1149:70b] * Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]'

Error seems is out of bound so section is accessing something that is not in array boundaries.

Code to access data in viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];

    testAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *managedObjectContext = [appDelegate managedObjectContext];

    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Menu"];
    NSString *cacheName = [@"Menu" stringByAppendingString:@"Cache"];

    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"categoryName" ascending:YES];
    NSPredicate* pred = [NSPredicate predicateWithFormat:@"section = %@", @"dining"];

    fetchRequest.predicate = pred; // req is the NSFetchRequest we're configuring
    [fetchRequest setSortDescriptors:@[sortDescriptor]];

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:cacheName ];

    NSError *error;
    if (![self.fetchedResultsController performFetch:&error])
    {
        NSLog(@"Fetch failed: %@", error);
    }
    else
    {
        NSLog(@"Records found: number %lu", (unsigned long)self.fetchedResultsController.fetchedObjects.count);     
    }

    NSFetchRequest *fetchRequestMenuTwo = [NSFetchRequest fetchRequestWithEntityName:@"Menu"];
    NSString *cacheNameTwo = [@"Menu" stringByAppendingString:@"Cache"];

    NSSortDescriptor *sortDescriptorTwo = [NSSortDescriptor sortDescriptorWithKey:@"categoryName" ascending:YES];
    NSPredicate* predTwo = [NSPredicate predicateWithFormat:@"section = %@", @"information"];

    fetchRequestMenuTwo.predicate = predTwo; // req is the NSFetchRequest we're configuring
    [fetchRequestMenuTwo setSortDescriptors:@[sortDescriptorTwo]];

    self.fetchedResultsControllerTwo = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequestMenuTwo managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:cacheNameTwo ];

    //NSError *error;
    if (![self.fetchedResultsControllerTwo performFetch:&error])
    {
        NSLog(@"Fetch failed: %@", error);
    }
    else
    {
        NSLog(@"Records found at 2nd fetch: number %lu", (unsigned long)self.fetchedResultsControllerTwo.fetchedObjects.count);
    }
}

Note: I wanted to create object in variable which I can easily access in further code but I tried all variable could not do it.

Code used to get sections:

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    switch (section) {
        case 0:
            return self.fetchedResultsController.fetchedObjects.count;
        case 1:
            return self.fetchedResultsControllerTwo.fetchedObjects.count;
        default:
            return 0;
    }
}

-(NSString *)nameForSection:(NSInteger)index
{
    switch(index)
    {
        case 0:
            return @"Section One";
        case 1:
            return @"Section Two";
        default:
            return @"Unknown";
    }
}

Finally getting everything in indexPath (here I am getting error as soon as I put 2nd section coding otherwise first section running ok:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *identifier = @"Cell";

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];

    switch (indexPath.section) {
        case 0:
        {
            Menu *menu = (Menu *)[self.fetchedResultsController objectAtIndexPath:indexPath];
            UIImageView *cityLivingImageView = (UIImageView *)[cell viewWithTag:100];
            UILabel *cityLivingLabel = (UILabel *)[cell viewWithTag:200];
            cityLivingLabel.text = menu.categoryName;
            cityLivingImageView.image    = [UIImage imageNamed:menu.imageName];

            NSLog(@"Record found: %@", menu.categoryName );

            return cell;
        }
        case 1:
        {
            Menu *menu = (Menu *)[self.fetchedResultsControllerTwo objectAtIndexPath:indexPath];
            UIImageView *cityLivingImageView = (UIImageView *)[cell viewWithTag:100];
            UILabel *cityLivingLabel = (UILabel *)[cell viewWithTag:200];
            cityLivingLabel.text = menu.categoryName;
            cityLivingImageView.image    = [UIImage imageNamed:menu.imageName];

            NSLog(@"Record found: %@", menu.categoryName );

            return cell;        
        }
        default:
        break;
    }

    return cell;    
}

I have only two sections and one has 5 rows and other have 6 rows. I wanted to make this one controller to access data and put everything in variables so later I can use variables. My database is categoryName | categoryImage | section

where section identify section one and two so I use predicate to make two different section using section field value.


Solution

  • First question is how many different values do your Menu objects have for the section key? Generally you should a single fetched results controller (FRC) and use section as the sectionNameKeyPath.

    Another issue with your FRC setup is that both use a cache with the same name. This won't work properly as the FRCs each have different predicates. Ensure the cache names are unique or remove the cache names and set to nil.

    Your crash comes because you are using 2 FRCs but you treat the second one like you only have 1. You are creating a collection view with 2 sections, but each FRC only understands one section. Your first section works because the section number (zero) matches what the first FRC expects. But, the second section has a section number of one, which doesn't match, so you get a range exception.

    You should change your code to only use one FRC if you can (if you want one section for each different value of section in the data store). If not, you can fix the crash by changing your code to:

    NSIndexPath *fabricatedIndexPath = [NSIndexPath indexPathForItem:indexPath.item inSection:0];
    Menu *menu = (Menu *)[self.fetchedResultsControllerTwo objectAtIndexPath:fabricatedIndexPath];
    

    (bit messy really)