Search code examples
iosobjective-cuitableviewcore-datansfetchedresultscontroller

NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1] error with CoreData and NSFRC


I have a simple application that is a two tabbed UITabBarController. The first Tab is called Languages and it contains a list of hard coded Languages in the app. In the prepareForSegue method, I am populating a list of Leaflets and Videos for that language by NSMutableArray and passing that successfully over to a new UITableViewController called LeafletsAndVideos. Some languages have Leaflets and Videos and some have just Leaflets, where Leaflets are in Section 0 and Videos are in Section 1.

With reference to this question: Changing the UIImage of a specific UITableViewCell when an event occurs, I have set up the use of "Favourting" specific cells in the LeafetsAndVideos. Through the use of Core Data, I am taking the title of the cell that was favourited, adding it to the Core Data model. The reason I do this is because the second tab is called Favourites and through the use of NSFetchedResultsController, I am populating this UITableView with any cells that have been "Favourited".

That park works really well.

The problem now is that when in the LeafletsAndVideos UITableView, I favourite a cell and it appears in the Favourites tab straight away through the use of NSFRC on the FavouritesTableViewController, but if I want to unfavorite the same cell, I want that to be deleted from the Favourites tab.

Issue

The issue is that when I unfavorite a cell, it is just added to the Favourites tab again.

For the sake of clarity, when I say "Favourite", let's say I am tapping on the cell (didSelectCell) and so when I tap on it again, it unfavorites.

The LeafletsAndTable UITableView has no reference to the NSFRC and it doesn't really need one because the cells are being populated by the NSMutableArray's set in the Languages UITableView (I totally understand I can just use Core Data for the titles, but that's complicated in this particular setup - Core Data is just being used as a mechanism to detect the titles of the Favourited cells).

Within my LeafletAndVideo UITableView, I have the following code when setting up the Favourites:

        NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
        CustomLeafletVideoTableViewCell *cell = (CustomLeafletVideoTableViewCell*)[self.tableView cellForRowAtIndexPath:indexPath];

        NSString *cellTitle = cell.customCellLabel.text;
        NSManagedObjectContext *context = [self managedObjectContext];            
        Favourites *favourites = [NSEntityDescription insertNewObjectForEntityForName:@"Favourites" inManagedObjectContext:context];
        favourites.title = cellTitle;
        NSString *key = [NSString stringWithFormat:@"%@_%ld_%ld", self.selectedLanguage, (long)indexPath.section, (long)indexPath.row];
        if (self.favoritesDict[key] == nil) {
            self.favoritesDict[key] = @(1);
        } else {
            [self.favoritesDict removeObjectForKey:key];
        }
        [[NSUserDefaults standardUserDefaults] setObject:self.favoritesDict forKey:@"favoritesDict"];
NSError *error = nil;

        if (![context save:&error])
        {
            // Error
        }

        [cell hideUtilityButtonsAnimated:YES];
        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];

Issue

With the code above, when unfavouriting a cell, it gets added again to the Favourites tab (which is fair enough because I'm not doing any deleting here).

So in the else statement of that code, I tried something. I remove the object from the dictionary but I am also calling the Favourites tab and the method completeFetchedResults:

        if (self.favoritesDict[key] == nil) {
            self.favoritesDict[key] = @(1);
        } else {
            [self.favoritesDict removeObjectForKey:key];
            FavouritesTableViewController *favTab = [[FavouritesTableViewController alloc] init];
            [favTab completeFetchedResults];
            [favTab moreButtonPressed:indexPath];

        }

This method is simply calling the fetch on the NSFRC.

- (void)completeFetchedResults
{
    NSError *error;
    if (![[self fetchedResultsController] performFetch:&error])
    {
        //exit(-1);
    }

}

I am then calling the method to delete the object by passing it the indexPath:

- (void)moreButtonPressed:(NSIndexPath *)indexPath
{
    NSManagedObjectContext *context = [self managedObjectContext];
    [self.managedObjectContext deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];

    NSError *error = nil;

    if (![context save:&error])
    {
        // Error
    }

}

When I run this, select a cell, it favourites it (as expected) and gets added to the Favourites tab. When I select the cell again (to remove the favourite), it crashes out with this error:

'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]'

When I place an exception breakpoint, it crashes at:

if (![context save:&error])
{
    // Error
}

In the moreButtonPressed method.

I'm not too sure how to fix this, but the desired effect is that when I select the cell, it favourites it (working) and when I select the cell again, it:

1) Deletes the object from the Favourites tab (not working)

Any guidance would really be appreciated.

Stack Trace Update

0   CoreFoundation                      0x000000010c588f45 __exceptionPreprocess + 165
1   libobjc.A.dylib                     0x000000010bc7adeb objc_exception_throw + 48
2   CoreFoundation                      0x000000010c477b14 -[__NSArrayI objectAtIndex:] + 164
3   Street Parchar                      0x000000010b5e8b37 -[FavouritesTableViewController moreButtonPressed:] + 151
4   Street Parchar                      0x000000010b5f2386 -[LeafletsAndVideosTableViewController swipeableTableViewCell:didTriggerRightUtilityButtonWithIndex:] + 1158
5   Street Parchar                      0x000000010b5e44d5 -[SWTableViewCell rightUtilityButtonHandler:] + 213
6   UIKit                               0x000000010d1e194f _UIGestureRecognizerSendTargetActions + 153
7   UIKit                               0x000000010d1ddfc1 _UIGestureRecognizerSendActions + 162
8   UIKit                               0x000000010d1dbfbe -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 843
9   UIKit                               0x000000010d1e4283 ___UIGestureRecognizerUpdate_block_invoke898 + 79
10  UIKit                               0x000000010d1e4121 _UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks + 342
11  UIKit                               0x000000010d1d1bdd _UIGestureRecognizerUpdate + 2634
12  UIKit                               0x000000010cd6f9c0 -[UIWindow _sendGesturesForEvent:] + 1137
13  UIKit                               0x000000010cd70bf6 -[UIWindow sendEvent:] + 849
14  UIKit                               0x000000010cd202fa -[UIApplication sendEvent:] + 263
15  UIKit                               0x000000010ccfaabf _UIApplicationHandleEventQueue + 6844
16  CoreFoundation                      0x000000010c4b5011 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
17  CoreFoundation                      0x000000010c4aaf3c __CFRunLoopDoSources0 + 556
18  CoreFoundation                      0x000000010c4aa3f3 __CFRunLoopRun + 867
19  CoreFoundation                      0x000000010c4a9e08 CFRunLoopRunSpecific + 488
20  GraphicsServices                    0x0000000111165ad2 GSEventRunModal + 161
21  UIKit                               0x000000010cd0030d UIApplicationMain + 171
22  Street Parchar                      0x000000010b5dfedf main + 111
23  libdyld.dylib                       0x00000001104ae92d start + 1
24  ???                                 0x0000000000000001 0x0 + 1

) libc++abi.dylib: terminating with uncaught exception of type NSException

Update 2

Including the fetchedResultsController in the FavouritesTab

- (NSFetchedResultsController *)fetchedResultsController
{
    NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
    if (_fetchedResultsController != nil)
    {
        return _fetchedResultsController;
    }
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];


    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Favourites" inManagedObjectContext:managedObjectContext];
    fetchRequest.entity = entity;
    NSPredicate *d = [NSPredicate predicateWithFormat:@"title != nil"];
    [fetchRequest setPredicate:d];
    NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"title" ascending:NO];


    fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
    fetchRequest.fetchBatchSize = 20;
    NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    self.fetchedResultsController = theFetchedResultsController;
    _fetchedResultsController.delegate = self;
    return _fetchedResultsController;
}

Solution

  • The problem is the new instance of FavouritesTableViewController which has no connection to the corresponding object in Interface Builder.

    You have to perform all Core Data related tasks in the LeafletsAndVideos controller like this code

    NSString *key = [NSString stringWithFormat:@"%@_%ld_%ld", self.selectedLanguage, (long)indexPath.section, (long)indexPath.row]; 
    if (self.favoritesDict[key] == nil) { 
        self.favoritesDict[key] = @(1); 
        Favourites *favourites = [NSEntityDescription insertNewObjectForEntityForName:@"Favourites" inManagedObjectContext:context]; 
        favourites.title = cellTitle; 
    } 
    else { 
        [self.favoritesDict removeObjectForKey:key]; 
        NSError *error; 
        NSFetchRequest *request = [[NSFetchRequest alloc] init]; 
        request.entity = [NSEntityDescription entityForName:@"Favourites" inManagedObjectContext: context]; 
        request.predicate = [NSPredicate predicateWithFormat:@"title == %@", cellTitle]; 
        NSArray *items = [context executeFetchRequest:request error:&error]; 
        if (error == nil && items.count) { 
            NSManagedObject *managedObject = items[0]; 
            [context deleteObject:managedObject]; 
        } 
    }
    

    When you switch to the Favourites tab and perform a fetch in viewWillAppear the data will be displayed correctly.