Re-Ordering NSFetchedResultsController
So Marcus is the Core Data guy, but I’ve been working with it a good bit myself lately and was recently faced with having to add re-ordering for a list of entities in a UITableView. The methods I found online for accomplishing this all suggested using an NSMutableArray as the data source for the table view. That will work, but I came up with another method, though similar, that achieved what I need without having to switch from using my NSFetchedResultsController as the data source behind the UITableView. In the end, I did use an NSMutableArray, however, I end up using it just to take advantage of its indexing. Read on to see what I mean.
Download the source code for the Favorite Things project.
A Few of My Favorite Things
My kids have been watching The Sound of Music lately, but that’s not what made me decide to use a list of favorite things as the premise of my example code (sorry if that just got a Julie Andrews song stuck in your head. For the rest of you who have no idea what I’m talking about, move along. Nothing to see here). What made me think of it is the fact that your favorite things might need re-ordered from time to time. A list of my favorite things all seem to be Apple products as the example code shows. Yours might be something else. I’ve added a method at the start of this example app to populate the Core Data database with a list of a few of my favorite things (“♫♫…when the dog bites, when the bee stings, when you’re feeling sad ♫♫… ooops, sorry). I first fetch the list and if it’s empty I go ahead and populate the Core Data data store. Here is that code:
- (void)loadFavoriteThingsData;
{
if ([[fetchedResultsController fetchedObjects] count] > 0)
return;
NSManagedObject *favoriteThing = [NSEntityDescription insertNewObjectForEntityForName:@"FavoriteThing" inManagedObjectContext:[self managedObjectContext]];
[favoriteThing setValue:@"MacBook Pro" forKey:@"thingName"];
[favoriteThing setValue:@"A powerful computer that will burn your lap." forKey:@"thingDescription"];
[favoriteThing setValue:[NSNumber numberWithInt:0] forKey:@"displayOrder"];
favoriteThing = [NSEntityDescription insertNewObjectForEntityForName:@"FavoriteThing" inManagedObjectContext:[self managedObjectContext]];
[favoriteThing setValue:@"iPad" forKey:@"thingName"];
[favoriteThing setValue:@"That's a really big iPod!" forKey:@"thingDescription"];
[favoriteThing setValue:[NSNumber numberWithInt:1] forKey:@"displayOrder"];
favoriteThing = [NSEntityDescription insertNewObjectForEntityForName:@"FavoriteThing" inManagedObjectContext:[self managedObjectContext]];
[favoriteThing setValue:@"iPhone" forKey:@"thingName"];
[favoriteThing setValue:@"A computer that thinks it's a phone." forKey:@"thingDescription"];
[favoriteThing setValue:[NSNumber numberWithInt:2] forKey:@"displayOrder"];
favoriteThing = [NSEntityDescription insertNewObjectForEntityForName:@"FavoriteThing" inManagedObjectContext:[self managedObjectContext]];
[favoriteThing setValue:@"iPod" forKey:@"thingName"];
[favoriteThing setValue:@"Also known as the iPad nano." forKey:@"thingDescription"];
[favoriteThing setValue:[NSNumber numberWithInt:3] forKey:@"displayOrder"];
favoriteThing = [NSEntityDescription insertNewObjectForEntityForName:@"FavoriteThing" inManagedObjectContext:[self managedObjectContext]];
[favoriteThing setValue:@"WWDC Ticket" forKey:@"thingName"];
[favoriteThing setValue:@"It sold out in eight days this year, you know?" forKey:@"thingDescription"];
[favoriteThing setValue:[NSNumber numberWithInt:4] forKey:@"displayOrder"];
[managedObjectContext save:nil];
}
Our Core Data database will now be populated with some initial data so we’ll have something to see. Here is what the initial screen looks like:
Display Order Attribute
In order to implement re-ordering, your entity in your Core Data model will need a displayOrder attribute (you can call it whatever you want, but I’ve named mine displayOrder). This is an integer that will keep track of your indexes and is the field you will use to sort your results in the fetch request sort descriptor. Here is what the code looks like to fetch the entities using the displayOrder attribute as the sort descriptor:
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController) return fetchedResultsController;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity =
[NSEntityDescription entityForName:@"FavoriteThing"
inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor =
[[NSSortDescriptor alloc] initWithKey:@"displayOrder"
ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc]
initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:nil cacheName:@"ThingsCache"];
aFetchedResultsController.delegate = self;
[self setFetchedResultsController:aFetchedResultsController];
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
Your Core Data entity that you want to re-order should look something like this in the data model editor in Xcode:
Favorites Change
This post is about re-ordering so here is the point. NSFetchedResultsController doesn’t have a built in way to re-order the results, so load them into an NSMutableArray, rearrange them there, and then re-iterate over the items once sorted setting each of their displayOrder field as you go. Then save the managed object context and you’re all re-ordered.
The reason we use an NSMutableArray is because we can insert and remove managed object pointers to/from the array without triggering any changes to the data store. When you make changes to the objects themselves the change is reflected immediately–which is often what we want, but sometimes we don’t as in this case. In Marcus’ Core Data book he points out that you can keep KVO messages from being sent when you want to change a managed object’s attributes by calling -setPrimitiveValue:forKey, but it seemed simpler to me to just re-arrange the objects in an array, and then make the change to each of the object’s displayOrder attribute. Here is the code you use to re-order the results:
- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath
toIndexPath:(NSIndexPath *)destinationIndexPath;
{
NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving.
NSManagedObject *thing = [[self fetchedResultsController] objectAtIndexPath:sourceIndexPath];
// Remove the object we're moving from the array.
[things removeObject:thing];
// Now re-insert it at the destination.
[things insertObject:thing atIndex:[destinationIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 0;
for (NSManagedObject *mo in things)
{
[mo setValue:[NSNumber numberWithInt:i++] forKey:@"displayOrder"];
}
[things release], things = nil;
[managedObjectContext save:nil];
}
Further Considerations
You may be thinking that if you iterate through every object in your results you’re actually loading them into memory. This is true, but not something to be concerned about for a couple reasons. First, the results were loaded into memory to display in the table view already. Also, in any table view where you are planning to re-order your results, it is highly unlikely that your list of objects will be very long as trying to re-order too many objects would just prove frustrating for your user and your design has just clearly demonstrated the need to be re-designed at that point anyhow.
Finishing Up
There are just a few more points I want to make before we’re done.
The project template I used is just the default navigation template along with Core Data for storage.
In order for the table view to display the detail text, you need to instantiate your UITableViewCells using the UITableViewCellStyleSubtitle constant. Here is the code to do so:
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:@"Cell"] autorelease];
}
Finally, remember that you have to implement -canMoveRowAtIndexPath and return YES if you want the re-order control to display:
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
Conclusion
The NSFetchedResulsController in tandem with a simple NSMutableArray works great to provide re-ordering for your table views. Until next time.
Download the source code for the Favorite Things project.
This is great—if only you’d written it a few weeks ago! I went through a lot of trouble trying to sort a list alphabetically by a ‘name’ attribute, but with with numbers last, like in the iPhone contacts app. Eventually did settle on adding a sortOrder attribute to my model, but it made me feel dirty (makes sense here, of course). Good to know that having a sortOrder attribute is basically just How It’s Done.
Hi!
good post. My preference is to lazily create the fetch request and the sort descriptors. Then when the list needs to be resorted, I will set the fetch request, sort descriptor, and then the fetchedResultsController to nil. The getter for the sortDescriptor then is setup depending on which ever property needs to be sorted. Then when the tableView or specific rows are reloaded the fetchedResultsController is reinitialized and the different sortDescriptor can be used.
Thanks Matt! I think I found a bug, however. If you re-order more than one item and then re-start the app, you’ll see that the ordering was not persisted correctly.
This seems to be caused by -tableView:moveRowAtIndexPath:toIndexPath: assuming that the array of entities returned by [fetchedResultsController fetchedObjects] is always sorted correctly–it isn’t.
Re-executing the fetch before (or after) your move logic does resolve the issue (here’s a full example: http://pastie.org/1054504 but is it the best fix? Hopefully more experienced Core Data folk will chime in…
Anyways, thanks to you and Marcus for providing such an amazing resource for everyone else–keep up the good work!
@Clint H
Thanks, Clint. I’ll take a closer look when I get a chance. I can see what you’re saying. It seems your solution is fine, but there may be a better way. Will see what I can find.
Thanks again.
-Matt
So, if I start with this and add in the code to add rows, everything blows up. Mind expanding the sample with some row adding and see if weird things happen to you? (And if not, sharing that code?)
Thanks
-Micah
@coopermj and @Clint. This should help…
The Apple docs mention that NSFetchedResultsController is designed to respond to changes at the model level. E.g. you change the model, save it and then the view automatically updates.
However when you re-order something using the tableview methods the view is already correct (because you’ve dragged and dropped the cell there) and so when the delegate detects your index changes, it walks all over your view believing the cells to be in their original position. The trick is to ‘disable’ the UI updates with a boolean in your delegate methods. Set the boolean just before your re-indexing and unset it afterwards.
For an example, see this stack overflow link
http://stackoverflow.com/questions/1077568/how-to-implement-re-ordering-of-coredata-records
For the Apple docs mention of user-driven updates, see this link
http://developer.apple.com/iphone/library/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40008228-CH1-SW14
Hope this helps,
Richard
I just needed to do this and was pondering a good way to do it. Thanks for this and the other articles I’ve read over the last few months!
I have a single entity model with some 6 attributes in it.
I have used table view to list all the datas and implemented the delegate protocols to respond to changes. I feel like I have implemented all the stuffs as it should be but the app gets stuck while deleting a row. While the app still is functional with the code the navigation around the ui element becomes impossible.
The application gets stuck at the place where the animation for the deletion of the row has just started and gets hung with the delete button and the deleting row still showing.
I get error as ,
An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (8), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted)