mvasco
mvasco

Reputation: 5107

After deleting last row of a section, the app crashes

In my iOS app ,I have a table view with sections, all populated from core data. If a delete the last row of a section, the app crashes. I am not able to find out where is the issue in my code, you are kindly requested to help me. Thank you in advance. Here is my code so far:

#import "PersonsTVC.h"
#import "Person.h"

@implementation PersonsTVC
@synthesize fetchedResultsController = __fetchedResultsController;
@synthesize managedObjectContext = __managedObjectContext;
@synthesize selectedPerson;
@synthesize searchResults,titulosseccion;



- (void)setupFetchedResultsController
{
    // 1 - Decide what Entity you want
    NSString *entityName = @"Person"; // Put your entity name here
    NSLog(@"Setting up a Fetched Results Controller for the Entity named %@", entityName);

    // 2 - Request that Entity
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];

    // 3 - Filter it if you want
    //request.predicate = [NSPredicate predicateWithFormat:@"Person.name = Blah"];

    // 4 - Sort it if you want
    // First sort descriptor (required for grouping into sections):
    NSSortDescriptor *sortByDate = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO];
    // Second sort descriptor (for the items within each section):
    NSSortDescriptor *sortByName = [[NSSortDescriptor alloc] initWithKey:@"firstname" ascending:YES];
    [request setSortDescriptors:@[sortByDate, sortByName]];


   //
   // request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"firstname"
                                                                                     //ascending:YES
                                                                                      //selector:@selector(localizedCaseInsensitiveCompare:)]];
    // 5 - Fetch it
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                        managedObjectContext:self.managedObjectContext
                                                                          sectionNameKeyPath:@"sectionIdentifier"
                                                                                   cacheName:nil];
    [self performFetch];
}




- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> theSection = [[self.fetchedResultsController sections] objectAtIndex:section];

    NSString *sectionName = [theSection name];
    if ([sectionName isEqualToString:@"0"]) {
        return @"Today";
    } else if ([sectionName isEqualToString:@"1"]) {
        return @"Tomorrow";
    }
    else if ([sectionName isEqualToString:@"2"]) {
        return @"Upcoming";
    }
    return @"Other";
}

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.

    return [[self.fetchedResultsController sections] count];
}



- (void) viewDidLoad
{



    self.searchResults = [NSMutableArray arrayWithCapacity:[[self.fetchedResultsController fetchedObjects] count]];
    [self.tableView reloadData];
}








- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self setupFetchedResultsController];
}




- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

    // Perform segue to detail when a SEARCH table cell is touched
    if(tableView == self.searchDisplayController.searchResultsTableView)
    {
        [self performSegueWithIdentifier:@"Person Detail Segue" sender:tableView];
    }

}



- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Persons Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    // Configure the cell...
    // Configure the cell...
    Person  *person = nil;

    if (tableView == self.searchDisplayController.searchResultsTableView)
    {
        NSLog(@"Configuring cell to show search results");
        person = [self.searchResults objectAtIndex:indexPath.row];
    }
    else
    {
        NSLog(@"Configuring cell to show normal data");
        person = [self.fetchedResultsController objectAtIndexPath:indexPath];
    }








    NSString *fullname = [NSString stringWithFormat:@"%@ %@", person.firstname, person.surname];
    cell.textLabel.text = person.firstname;
    if ([person.inRole.color isEqual :@"Yellow"])
    {
        cell.imageView.image = [UIImage imageNamed:@"Yellow"];
    }
    if ([person.inRole.color isEqual :@"Black"])
    {
        cell.imageView.image = [UIImage imageNamed:@"Black"];
    }
    if ([person.inRole.color isEqual :@"Grey"])
    {
        cell.imageView.image = [UIImage imageNamed:@"Grey"];
    }
    if ([person.inRole.color isEqual :@"Red"])
    {
        cell.imageView.image = [UIImage imageNamed:@"Red"];
    }
    if ([person.inRole.color isEqual :@"Blue"])
    {
        cell.imageView.image = [UIImage imageNamed:@"Blue"];
    }
    if ([person.inRole.color isEqual :@"Dark Green"])
    {
        cell.imageView.image = [UIImage imageNamed:@"DarkGreen"];
    }
    if ([person.inRole.color isEqual :@"Light Green"])
    {
        cell.imageView.image = [UIImage imageNamed:@"LightGreen"];
    }
    if ([person.inRole.color isEqual :@"Light Blue"])
    {
        cell.imageView.image = [UIImage imageNamed:@"LightBlue"];
    }
    if ([person.inRole.color isEqual :@"Brown"])
    {
        cell.imageView.image = [UIImage imageNamed:@"Brown"];
    }
    if ([person.inRole.color isEqual :@"Dark Orange"])
    {
        cell.imageView.image = [UIImage imageNamed:@"DarkOrange"];



    }

    NSDate *fechasinformat = person.date;
    NSString *fecha0 = [NSString stringWithFormat:@"%@", fechasinformat];


   cell.detailTextLabel.text = fecha0;



    return cell;
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (tableView == self.searchDisplayController.searchResultsTableView)
    {
        return [self.searchResults count];
    }
    else
    {
        return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
    }

}


- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {




    if (editingStyle == UITableViewCellEditingStyleDelete) {

        [self.tableView beginUpdates]; // Avoid  NSInternalInconsistencyException

        // Delete the person object that was swiped
        Person *personToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
        NSLog(@"Deleting (%@)", personToDelete.firstname);
        [self.managedObjectContext deleteObject:personToDelete];
        [self.managedObjectContext save:nil];

        // Delete the (now empty) row on the table
        [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];




        [self performFetch];





        [self.tableView endUpdates];
    }
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"Add Person Segue"])
    {
        NSLog(@"Setting PersonsTVC as a delegate of PersonDetailTVC");
        PersonDetailTVC *personDetailTVC = segue.destinationViewController;
        personDetailTVC.delegate = self;

        NSLog(@"Creating a new person and passing it to PersonDetailTVC");
        Person *newPerson = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                          inManagedObjectContext:self.managedObjectContext];

        personDetailTVC.person = newPerson;
    }
    else if ([segue.identifier isEqualToString:@"Person Detail Segue"])
    {
        NSLog(@"Setting PersonsTVC as a delegate of PersonDetailTVC");
        PersonDetailTVC *personDetailTVC = segue.destinationViewController;
        personDetailTVC.delegate = self;

        // Store selected Person in selectedPerson property
        if(sender == self.searchDisplayController.searchResultsTableView)
        {
            NSIndexPath *indexPath = [self.searchDisplayController.searchResultsTableView indexPathForSelectedRow];
            self.selectedPerson = [self.searchResults objectAtIndex:[indexPath row]];
        }
        else
        {
            NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
            self.selectedPerson = [self.fetchedResultsController objectAtIndexPath:indexPath];
        }

        NSLog(@"Passing selected person (%@) to PersonDetailTVC",    self.selectedPerson.firstname);
        personDetailTVC.person = self.selectedPerson;
    }
    else
    {
        NSLog(@"Unidentified Segue Attempted!");
    }
}

- (void)theSaveButtonOnThePersonDetailTVCWasTapped:(PersonDetailTVC *)controller
{
    // do something here like refreshing the table or whatever

    // close the delegated view
    [controller.navigationController popViewControllerAnimated:YES];    
}

#pragma mark -
#pragma mark Content Filtering

-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
    self.searchResults = [[self.fetchedResultsController fetchedObjects] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
        Person* person = evaluatedObject;
        NSString* firstName = person.firstname;

        //searchText having length < 3 should not be considered
        if (!!searchText && [searchText length] < 3) {
            return YES;
        }

        if ([scope isEqualToString:@"All"] || [firstName isEqualToString:scope])  {
            return ([firstName rangeOfString:searchText].location != NSNotFound);
        }
        return NO; //if nothing matches
    }]];
}

#pragma mark -
#pragma mark UISearchDisplayController Delegate Methods

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
    [self filterContentForSearchText:searchString scope:@"All"];
    return YES;
}

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:@"All"];
    return YES;
}


@end

Upvotes: 2

Views: 1340

Answers (3)

Dan Shelly
Dan Shelly

Reputation: 6011

A proper solution would be to make your PersonsTVC a delegate of the fetched results controller.
If you open an empty project that use CoreData you will have better understanding and probably find the solution to what you try to accomplish.

In its most simple form your code should be:

- (void)setupFetchedResultsController
{
    //Setup your fetch request ...

    NSFetchedResultsController* controller =
    [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                        managedObjectContext:self.managedObjectContext
                                          sectionNameKeyPath:@"sectionIdentifier"
                                                   cacheName:nil];
    //setup the delegation and retain the FRC
    controller.delegate = self;
    self.fetchedResultsController = controller;

    NSError* error = nil;
    if (![controller performFetch:&error]) {
        NSLog(@"error performing fetch: %@",error);
        abort();
    }
    //Not sure what this does, seems redundant
    //[self performFetch];
}

Now you must access your managed object through the FRC you created (not sure why you need self.searchResults).

As I said, in the simplest form, implement:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    // In the simplest, most efficient, case, reload the table view.
    [self.tableView reloadData];
}

Note: your deletion code should be:
This is also the reason your application crashed, as you updated the table before the FRC was able to report changes in the context.

- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
        [context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];

        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"Unresolved error %@", error);
            abort();
        }
    }   
}

Upvotes: 0

Duncan Groenewald
Duncan Groenewald

Reputation: 8988

To implement the NSFetchedResultsController delegate methods add the code below to your viewController (I stripped some code out so check it compiles), and set the viewController as the delegate using something like _fetchedResultsController.delegate = self; when you create the controller (in setupFetchedResultsController).

Any time managedObjects that are in the fetchedResultsController result set are added, modified or deleted these methods are called. You can see that they include the necessary calls to the UITableView to update the display. These are used in conjunction with using the fetchedResultsController as the data source for the UITableView - that way you won't get any inconsistency errors.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    NSLog(@"controllerWillChangeContent: called");
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{   NSLog(@"didChangeSection: called");
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    NSLog(@"controller:didChangeObject: called");
    NSLog(@" object is %@", [anObject valueForKey:@"displayName"]);
        UITableView *tableView = self.tableView;

        switch(type) {
            case NSFetchedResultsChangeInsert:
            {    NSLog(@"NSFetchedResultsChangeInsert: called");
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];

            }
                break;

            case NSFetchedResultsChangeDelete:
            {   NSLog(@"NSFetchedResultsChangeDelete: called");
                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];

            }
                break;

            case NSFetchedResultsChangeUpdate:
            {   NSLog(@"NSFetchedResultsChangeUpdate: called");
                [self configureCell:tableView cell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            }
                break;

            case NSFetchedResultsChangeMove:
            {   NSLog(@"NSFetchedResultsChangeMove: called");
                self.changedObjectIndex = newIndexPath;

                [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
                [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            }
                break;
        }

}

// NOTE: This is overridden by subclasses to modify the default behaviour
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    NSLog(@“controllerDidChangeContent: called");
        [self.tableView endUpdates];
}

- (void)configureCell:(UITableView *)tableView cell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    NSManagedObject *object;

    object = [self.fetchedResultsController objectAtIndexPath:indexPath];

    cell.textLabel.text = [[object valueForKey:@"displayName"] description];
    bool found =[[object valueForKey:@"found"] boolValue];

    if (found) {
        cell.textLabel.textColor = [UIColor greenColor];
    }

}

Upvotes: 1

gWiz
gWiz

Reputation: 1284

The error clearly indicates that you deleted 1 section, you had 1 before the deletion and you have 1 after the deletion. This is the problem. So probably your

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView

returns an incorrect number of sections after deleting last object.

Either your [[self.fetchedResultsController sections] count] is not 0 after deleting last object, or numberOfSectionsInTableView:(UITableView *)tableView is called by the self.searchDisplayController.searchResultsTableView and you don't check for that.

EDIT: To solve the issue add the following code:

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    if(tableView==self.searchDisplayController.searchResultsTableView)
        NSLog("search table asking");
    else
        NSLog("main table asking");
    NSLog("fetched sections: %d", [[self.fetchedResultsController sections] count]);
    return [[self.fetchedResultsController sections] count];
}

and post the console output when the crash occurs.

Upvotes: 2

Related Questions