Tuesday 2 February 2010

Reordering rows in a UITableView with Core Data

I've read a lot of things about how best to handle reorderable rows in a NSFetchedResultsController-backed UITableView, and I think I've hit upon a decent solution. This post only works with a single section table. (I may post some ideas about multi section stuff in a future post.)

Core Data doesn't have any native handling of user-reorderable objects. Like a database, it lets you apply a sort ordering to a query. So, to allow reordering, we need an attribute called sortOrder, which represents this user generated ordering.

In the viewDidLoad method of your UITableViewController subclass, set up your NSFetchedResultsController.


- (void)viewDidLoad
{
[super viewDidLoad];

NSManagedObjectContext *managedObjectContext = self.managedObjectContext;

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"sortOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];

NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:[NSEntityDescription entityForName:@"Drawer"
inManagedObjectContext:managedObjectContext]];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"specialType = nil"]];

self.fetchController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil
cacheName:@"drawersOrganiserCache"];

self.fetchController.delegate = self;

NSError *error;
BOOL success = [self.fetchController performFetch:&error];
if (!success)
{
// Handle error
}
}


Then set up the table to display the results from the NSFetchedResultsController in the usual way (this is not a full Core Data tutorial!).

The first part of our reordering magic comes in the tableView: moveRowAtIndexPath: toIndexPath: method. This method is called by the table view when a user has done some reordering.


- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
self.inReorderingOperation = YES;

NSMutableArray *array = [[self.fetchController fetchedObjects] mutableCopy];
id objectToMove = [[array objectAtIndex:fromIndexPath.row] retain];
[array removeObjectAtIndex:fromIndexPath.row];
[array insertObject:objectToMove atIndex:toIndexPath.row];
[objectToMove release];


for (int i=0; i<[array count]; i++)
{
[(NSManagedObject *)[array objectAtIndex:i] setValue:[NSNumber numberWithInt:i] forKey:@"sortOrder"];
}
[array release];

self.inReorderingOperation = NO;

NSError *error;
BOOL success = [self.fetchController performFetch:&error];
if (!success)
{
// Handle error
}

success = [[self managedObjectContext] save:&error];
if (!success)
{
// Handle error
}

}


So, what's going on here then? The table view sends us two index paths: where the row was moved from, and where the row was moved to. At this moment in time, the UI for the table has been updated (the table view took care of that itself), and the model has not been touched. So the row we want is currently at fromIndexPath in the model.


The first thing to do is set a property to note that you're within a reordering operation. I'll explain this a little more later, but it prevents a loop where the model updates the view and vice versa.


Next, we make a mutable copy of the array of fetched objects. Then, we isolate the object we want, remove it from that array, and add it back in the new place (based on the new index path). Pretty simple, actually.


Next, we need to update the sort ordering properties of the objects. We have an array of them in the correct order, so I just loop through them all in turn, setting each sort order incrementally. If your table is very large, you could optimise this by only updating the sort orderings for rows between the two index paths, as the rest won't have changed. My table was small enough I didn't need to worry about that.


Finally, set the reordering flag back to false, since you've made your updates. Then perform first a fetch then a save. I'm not entirely sure why the fetch needs to be performed first, but it fixed a ton of crazy bugs when I did it!


The last piece of the puzzle: handling your NSFetchedResultsController delegate methods.


- (void)controller:(NSFetchedResultsController*)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath*)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath*)newIndexPath
{
switch (type)
{
case NSFetchedResultsChangeMove:
if (!self.inReorderingOperation)
{
// do stuff to respond to model change
}
break;
}
}


That's only a subset of it. The bit we're interested in is NSFetchedResultsChangeMove. This is called when an item in your model has changed where it is in the order. This of course would happen when you update the sort ordering fields of the objects. What we do here is check that reordering flag that we set before. If it's set to true, we do nothing here (as the GUI has already been updated to the new ordering). If it's false, then the change came from elsewhere so we need to handle that change.

11 comments:

  1. Hi, thank you for posting this information. I'm just getting started on my first Cocoa app. I followed your instructions and they worked flawlessly.

    Did you ever figure out the explanation as to why you must fetch from the results controller before saving into the managed context?

    ReplyDelete
  2. No, although I haven't tried that hard to work it out — once I'd realised it worked, I moved on to other problems :)

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Please disregard or delete the last two comments, I spoke before I verified that it worked.

    ReplyDelete
  6. It's really a good and useful piece of information regarding reordering rows. Thank you for sharing. reorder-checks

    ReplyDelete
  7. Amazing post! Check designs of late are available in associate nearly unlimited style of styles. really of late you'll be able to add your own styles or photos to customize your checks. Thanks.
    Read More

    ReplyDelete
  8. GREAT SOLUTION! Thank you for sharing!

    ReplyDelete
  9. Nice Post! If you’re using your checks for business, don’t worry because there are some people who’ve decided to go for a more formal look by keeping the original design of their checks, except they’ve added a few more helpful information that their clients can use.
    NFL Football Teams

    ReplyDelete
  10. The only good reason to order from your bank is that some banks will let you pick up your checks from the bank.Do this if you are paranoid about someone stealing your checks out of your mailbox or in route.
    Find your bank

    ReplyDelete
  11. This article will teach you easy ways to go about the process of ordering and reordering checks. Be it for personal or business use, you will never have to pass this duty to some other person or procrastinate and delay reordering checks again.
    Read More


    ReplyDelete