Search code examples
uitableviewcore-datadelete-row

Algorithm for inserting/deleting cells in UITableView based on Core Data object?


I have a core data recipe object that contains an ordered list of ingredient objects.

The ingredients are displayed as a list in a UITableView. When the user cancels editing of the table view, I call rollback on the MOC, which may restore some ingredients (any that the user has deleted) and remove others (any that the user has added). I would like to animate the insertion/deletion so that the transition isn't jarring.

This is a bit harder than it seems at first, particularly since the UITableView will hack up a hairball if you both insert and remove the same cell.

Is there a bit of sample code out there that would help steer me in the right direction? Right now I have a ridiculously complicated setup using NSMutableSets that isn't quite working right.

NSMutableArray *preIngredients = [NSMutableArray arrayWithArray:[recipe orderedIngredients]];

[self.recipe.managedObjectContext rollback];

NSMutableArray *postIngredients = [NSMutableArray arrayWithArray:[recipe orderedIngredients]];

NSMutableSet *beforeIngredients = [NSMutableSet setWithArray:preIngredients];
NSMutableSet *afterIngredients = [NSMutableSet setWithArray:postIngredients];

NSMutableSet *restoredIngredients = [NSMutableSet setWithSet:afterIngredients];
[restoredIngredients minusSet:beforeIngredients];

NSMutableSet *removedIngredients = [NSMutableSet setWithSet:beforeIngredients];
[removedIngredients minusSet:afterIngredients];

NSMutableSet *allIngredients = [NSMutableSet setWithSet:beforeIngredients];
[allIngredients unionSet:afterIngredients];

int whatToDo[[preIngredients count]];
for (int i = 0; i < [preIngredients count]; i++)
    whatToDo[i] = 0;

for (Ingredient *ingredient in preIngredients) {
    int row = [preIngredients indexOfObject:ingredient];

    if ([removedIngredients containsObject:ingredient])
        whatToDo[row]--;

    if ([restoredIngredients containsObject:ingredient])
        whatToDo[row]++;
}

for (int i = 0; i < [preIngredients count]; i++) {
    if (whatToDo[i] < 0)
        [rowsToRemove addObject:[NSIndexPath indexPathForRow:i inSection:0]];
    else if (whatToDo[i] > 0)
        [rowsToRestore addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}

    // Also remove the "add new ingredient" cell
NSIndexPath *insertNewCellIndexPath = [NSIndexPath indexPathForRow:[preIngredients count] inSection:0];
if ([rowsToRestore indexOfObjectIdenticalTo:insertNewCellIndexPath] == NSNotFound)
    [rowsToRemove addObject:insertNewCellIndexPath];
else
    [rowsToRestore removeObjectIdenticalTo:insertNewCellIndexPath];

[self.tableView insertRowsAtIndexPaths:rowsToRestore withRowAnimation:UITableViewRowAnimationTop];
[self.tableView deleteRowsAtIndexPaths:rowsToRemove withRowAnimation:UITableViewRowAnimationTop];

Solution

  • Here is the working code, making heavy use of sets. If you know of a way to simplify it, don't hesitate to speak up :)

    [self.tableView beginUpdates];
    
    NSMutableArray *preIngredients = [NSMutableArray arrayWithArray:[recipe orderedIngredients]];
    [self.recipe.managedObjectContext rollback];
    NSMutableArray *postIngredients = [NSMutableArray arrayWithArray:[recipe orderedIngredients]];
    
    NSMutableSet *beforeIngredients = [NSMutableSet setWithArray:preIngredients];
    NSMutableSet *afterIngredients = [NSMutableSet setWithArray:postIngredients];
    
    NSMutableSet *ingredientsToRestore = [NSMutableSet setWithSet:afterIngredients];
    [ingredientsToRestore minusSet:beforeIngredients];
    
    NSMutableSet *ingredientsToRemove = [NSMutableSet setWithSet:beforeIngredients];
    [ingredientsToRemove minusSet:afterIngredients];
    
    NSMutableSet *indexPathsToRestore = [NSMutableSet setWithCapacity:[ingredientsToRestore count]];
    NSMutableSet *indexPathsToRemove = [NSMutableSet setWithCapacity:[ingredientsToRemove count]];
    
    for (Ingredient *ingredient in ingredientsToRemove)
        [indexPathsToRemove addObject:[NSIndexPath indexPathForRow:[preIngredients indexOfObject:ingredient] inSection:0]];
    
    // Also remove the "add new ingredient" row
    [indexPathsToRemove addObject:[NSIndexPath indexPathForRow:[preIngredients count] inSection:0]];
    
    for (Ingredient *ingredient in ingredientsToRestore)
        [indexPathsToRestore addObject:[NSIndexPath indexPathForRow:[postIngredients indexOfObject:ingredient] inSection:0]];
    
    NSMutableSet *commonIndexPaths = [NSMutableSet setWithSet:indexPathsToRemove];
    [commonIndexPaths intersectSet:indexPathsToRestore];
    
    [indexPathsToRemove minusSet:commonIndexPaths];
    [indexPathsToRestore minusSet:commonIndexPaths];
    
    [self.tableView insertRowsAtIndexPaths:[indexPathsToRestore allObjects] withRowAnimation:UITableViewRowAnimationTop];
    [self.tableView deleteRowsAtIndexPaths:[indexPathsToRemove allObjects] withRowAnimation:UITableViewRowAnimationTop];
    
    [self.tableView endUpdates];