NSFetchedResultsController is a really handy class. Use one of the default Core Data templates in Xcode and you’ll very quickly have a nice list of managed objects in a table view. With a few more lines of code you can get the NSFetchedResultsController to group your objects by sections. You do this by specifying a key-path in the class’s constructor method but there is another step that if overlooked will cause some confusion.
In a sample app I’ve created a food table that lists food in categories.
FetchedResultsController method grouping sections using a key-path:
Objective-C:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create and configure a fetch request with the food entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"RWFood" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Create the sort descriptors array.
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:nameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:@"category" cacheName:@"Food"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
// Memory management.
[aFetchedResultsController release];
[fetchRequest release];
[nameDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
Swift:
lazy var fetchedResultsController: NSFetchedResultsController<rwfood> = {
let fetchRequest: NSFetchRequest<rwfood> = RWFood.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "category", cacheName: "Food")
aFetchedResultsController.delegate = self
do {
try aFetchedResultsController.performFetch()
} catch let error {
print("Unable to perform fetch: \(error)")
}
return aFetchedResultsController
}()
Specify a key-path
Save and quit the app a few times and you’ll see the objects seem to be in the wrong sections. If you look closer you’ll see that the objects are actually sorted in ascending name order. On looking at the code, it seems this is exactly what we asked the program to do! After some testing it also seems to show up more often if the table is a grouped one.
As per the docs, after you specify a key-path to group each section with you also need to make sure the first sort descriptor is sorting this key-path. Add a sort descriptor and everything will work as expected.
Revised fetchedResultsController method with missing sort descriptor:
Objective-C:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create and configure a fetch request with the plant entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"RWPlant" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Create the sort descriptors array.
NSSortDescriptor *typeDescriptor = [[NSSortDescriptor alloc] initWithKey:@"type" ascending:YES];
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:typeDescriptor, nameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:@"type" cacheName:@"Plants"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
// Memory management.
[aFetchedResultsController release];
[fetchRequest release];
[categoryDescriptor release];
[nameDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
Swift:
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest: NSFetchRequest<rwplant> = RWPlant.fetchRequest()
let typeDescriptor = NSSortDescriptor(key: "type", ascending: true)
let nameDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [typeDescriptor, nameDescriptor]
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "type", cacheName: "Plants")
aFetchedResultsController.delegate = self
do {
try aFetchedResultsController.performFetch()
} catch let error {
print("Unable to perform fetch: \(error)")
}
return aFetchedResultsController
}()
Where are the section headers “Fruit” and “Vegetable” coming from?
How do you set up the relationship between a manually created section header and a core data entity?
Thanks
Hi AD,
Fruit and Vegetables are values of the type property you’d create on the entity.
So with the RWFruit entity that I’ve got in my example, it has a type property and a name property. All the fruits have the type set to “Fruit” and all the vegetables have the the type set to “Vegetable”. Then you set the sectionKeyPath to “type” and it will group all the objects that have matching types.
Hope that helps explain it, I might upload an example project if it helps.
How can we give custom section names without having the “type” attribute
Thanks. Saved my day. :-) Well written and logical once you think about it.
I’m glad I could help Denis. I’m trying to jot down as many of these little gotchas as I encounter them.
Thanks a lot, that saved me a lot of time. It’s logical once you know it!
THANK YOU! Had already wasted 2h on this when I came across your explanation. Many, many thanks.
How do you not show the “quick search index sidebar” on the right of the screen? When ever is set “sectionNameKeyPath” i get the first letter capitalized with a scroll bar. Noticed yours doesn’t have this.
Thanks in advanced!
Hi Pyraego, you must have implemented – (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView in your UITableViewDataSource?
AHA yes! Just searched that method and found it in “CoreDataTableViewController”. Now I just need to figure out how to remove this. This file is a template I found when going through core-data tutorials.
Got it working!! Ended up returning nil for that method. Thanks for all the help. You can delete the previous post if you want. Sorry for posting it “anonymous”
Thanks. Saved my night.
Wow! you really save my day!
Thank you man
Thanks man! This was driving me crazy. Glad I found you post. Saved me a ton of time!
Thanks man! This was driving me crazy. You saved my day. Thanks for taking the time to post and help others.
Thanks, this was EXTREMELY useful.
> If this **sectionNameKeyPath** is not the same as that specified by the **first sort descriptor** in fetchRequest, they **must** generate the **same relative orderings**. For example, the first sort descriptor in fetchRequest might specify the key for a persistent property; sectionNameKeyPath might specify a key for a transient property derived from the persistent property.
Yep, saved my ass too. Thanks!
Thanks so much. I was struggling with this section info keypath issue for more than a week.
Its kind of illogical by Apple that even after mentioning group by section names, it is grouping objects in wrong sections. Adding an extra sort key fixed for me.
Thanks!
Even though this is an old post, and in ObjC rather then Swift it just saved me from a couple hours of headache
Thanks a lot
Thanx so much