views:

27

answers:

2

I'm working on a feed reader iOS project. Feed entries are listed in a UITableView in reverse chronological order. On launch, they're loaded from a database into an array.

When the app syncs to feeds, it creates a new array for the new order of things, and then to update the table, compares the new array to the old array to determine what cells to delete, update, or insert. The way I've done it is naïve, and therefor really inefficient: Lots of calls to indexOfObject: to see if an item in one array is in the other array. Twice. once for each new entry as it's added to the new array, to see if it's in the old array, and then once for each entry in the old array, to see if it's not in the new array.

As a database professional, this design offends me.

But it must be a pretty common pattern. What would be the most appropriate, sufficiently Cocoa-ish way to go about this?

A: 

Consider using NSSets for differencing the set of current items and the set of new items, with a single NSMutableArray to hold the ordered current list. You would probably want to remove each of the expired items from the array, then insort each of the unexpired new items into the array. The items that you needed neither to remove nor to insort are the items that you may want to update.

Peter Hosey
A: 

Turns out I was coming at this wrong. The solution I found was to add and remove items from the array as usual, and then to call insertRowsAtIndexPaths:withRowAnimation:, reloadRowsAtIndexPaths:withRowAnimation:, and deleteRowsAtIndexPaths:withRowAnimation: as appropriate for each row added, changed, or moved. The fault in my previous plan was the thought that I should wait until all the changes and been made, and then call each of those methods only once in a beginUpdates/endUpdates block. Turns out the block wasn't actually necessary, as the modification methods can be called outside of them.

It was much easier to call each method once for each cell inserted, updated, or deleted, than to calculate all the changes at the end and commit them at once. Was just too confusing, error-prone, and inefficient to try to do it all at once.

So the code I ended up with looks like this:

if (parsedItem.savedState == ItemModelSavedStateInserted) {
    // It's a new entry. Insert it.
    [items addObject:parsedItem];
    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:items.count - 1 inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
} else {
    // It's an existing entry. Find it in the portal and move it, if necessary.
    NSUInteger foundAt = [items
        indexOfObject:parsedItem
              inRange:NSMakeRange(currentItemIndex, items.count - currentItemIndex - 1)
    ];
    if (foundAt == currentItemIndex) {
        // It hasn't moved!
        if (parsedItem.savedState == ItemModelSavedStateUpdated) {
            // It was updated, so replace it.
            [items replaceObjectAtIndex:currentItemIndex withObject:parsedItem];
            [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:currentItemIndex inSection:0]] withRowAnimation:UITableViewRowAnimationMiddle];
        }
    } else {
        // It has shifted position.
        if (foundAt != NSNotFound) {
            // It has moved.
            [items removeObjectAtIndex:foundAt];
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:foundAt inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
        }
        // Need to insert it.
        [items insertObject:parsedItem atIndex:currentItemIndex];
        [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:currentItemIndex inSection:0]] withRowAnimation:UITableViewRowAnimationTop];
    }
}
Theory