Question

I've got a UITableView backed by an array of Core Data objects, made by appending two arrays of Core Data objects named folders and notes. Whenever I try to delete a row from the table view, it throws:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

Here's the code to delete (in my UITableViewCell subclass):

NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
[object MR_deleteInContext:context];
[context MR_saveToPersistentStoreAndWait];
[tableViewController loadData];
[tableView deleteRowsAtIndexPaths:@[self.indexPath] withRowAnimation:UITableViewRowAnimationLeft];

Note: the MR_ methods are MagicalRecord methods, they do what they say.

Here's the loadData method in the UITableViewController (basically refetches the array):

NSPredicate *textParentFilter = [NSPredicate predicateWithFormat:@"parent == %@", self.parent];
self.notes = [Note MR_findAllWithPredicate:textParentFilter];
NSPredicate *folderParentFilter = [NSPredicate predicateWithFormat:@"parentFolder == %@", self.parent];
self.folders = [Folder MR_findAllWithPredicate:folderParentFilter];

Also:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    // Return the number of rows in the section.
    return [self.notes count] + [self.folders count];
}

I can see why I'm getting an exception: I'm deleting the row in loadData and then the numberOfRowsInSection is out of sync from the tableview and the Core Data store. However, if I replace deleteRowsAtIndexPaths with [tableView reloadData], it deletes just fine (but there's no animation). I've tried beginUpdates and endUpdates, switching the order in which things are deleted, to no avail.

Was it helpful?

Solution 3

I've fixed it! Or rather, NSFetchedResultsController fixed it. I've implemented

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath

in my view controller to deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:. My delete method is now just

NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext]; [object MR_deleteInContext:context]; [context MR_saveToPersistentStoreAndWait];

(thanks for the advice about MR_contextForCurrentThread).

Here's loadData:

NSManagedObjectContext *c = [NSManagedObjectContext MR_defaultContext]; NSPredicate *textParentFilter = [NSPredicate predicateWithFormat:@"parent == %@", self.parent]; self.notes = [Note MR_fetchAllSortedBy:@"text" ascending:YES withPredicate:textParentFilter groupBy:nil delegate:self inContext:c]; NSPredicate *folderParentFilter = [NSPredicate predicateWithFormat:@"parentFolder == %@", self.parent]; self.folders = [Folder MR_fetchAllSortedBy:@"title" ascending:YES withPredicate:folderParentFilter groupBy:nil delegate:self inContext:c];

And - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section:

if ([self.notes.sections count] + [self.folders.sections count] > 0) { id <NSFetchedResultsSectionInfo> notesSectionInfo = [self.notes.sections objectAtIndex:section]; id <NSFetchedResultsSectionInfo> foldersSectionInfo = [self.folders.sections objectAtIndex:section]; return [notesSectionInfo numberOfObjects] + [foldersSectionInfo numberOfObjects]; } else return 0;

OTHER TIPS

When working with UITableViews and Core Data, it is important that the objects in your storage (e.g., your notes and folders arrays) match the UITableView delegate/datasource calls at all times. When you start animating cells for addition, deletion and updating, you encounter synchronisation issues where occasionally, the values don't align. A good example is when you have to perform multiple cell additions/deletions in succession while showing or revealing a section of a table. If you use [tableView reloadData], you enforce synchronisation but won't be able to animate the cells.

NSFetchedResultsController can be used to synchronise multiple cell additions/deletions with animations. It will require a bit of rewiring on your end, but not as much as you'd think. With this, you can control animation styles, even bulk modify cells while keeping your table intact. NSFetchedResultsController will listen to changes in Core Data based on a predicate, and reflect these changes in the tableview. This means you can have multiple tableviews respond with their own NSFetchedResultsController without having to notify them, and everything will stay synchronised.

A good place to find skeleton code is to set up a new Core Data project in XCode. Methods that you'll need to add to your code (found in MasterViewController.m):

// this references your controller that listens to Core Data and communicates with the tableview
- (NSFetchedResultsController *)fetchedResultsController 

// delegate methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
       atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath;
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

Your tableview will then need to consult the NSFetchedController instance for it's delegate/datasource methods. For example:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
    return [sectionInfo numberOfObjects];
}

If you're feeling advanced enough, there's a great tutorial that shows how you can queue cells additions/deletions for improved performance:

http://www.fruitstandsoftware.com/blog/2013/02/uitableview-and-nsfetchedresultscontroller-updates-done-right/

NSFetchedResultsController is one of those things that you don't really hear about in a basic Core Data tutorial, but it proves to be a real asset when doing table animations and handling large amounts of data (through caching).

I use the following code whenever a user slides a cell to delete it:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        if (tableView == self.tableView)
        {
           NSInteger row = [indexPath row];
           Entry *e = self.entries[row];

           [self.entries removeObjectAtIndex: indexPath.row];

           [e MR_deleteEntity];
           [self.tableView reloadData];
        }
    }
}

In your case you first need to figure out if 'row' corresponds to a Note or a Folder, but the idea is the same.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top