Sorting Core Data results into NSFetchedResultsController - iphone

I have an entity Tag with string property tagName. I went to fetch all objects in this Entity into a NSFetchedResultsController, but I want Tag with tagName "Main" to be the first object. Here's what I'm doing now:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Tag" inManagedObjectContext:appDelegate.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *lastDescriptor2 =
[[[NSSortDescriptor alloc] initWithKey:#"tagName" ascending:NO comparator:^NSComparisonResult(NSString* tag1, NSString* tag2) {
NSLog(#"compare");
if ([tag1 isEqualToString:#"main"]) return NSOrderedAscending;
if ([tag2 isEqualToString:#"main"]) return NSOrderedDescending;
return [tag1 compare:tag2];
}] autorelease];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:lastDescriptor2]];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:appDelegate.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
self.fetchedResultsController.delegate=self;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
This code is called in my viewDidLoad method, and is only called once. The first time it's called, for some reason the sortDescriptor here doesn't apply - it just doesn't get called (the NSLog statement doesn't show up either). My results are returning solely based on the BOOL value I specify for ascending - the block is ignored.
But when I insert a new Tag object into the MOC and the NSFetchedResultsController update delegate methods are called, the actual sortDescriptor gets applied, and the NSLog(#"compare") finally appears, but only when I make updates to the objects! No matter what I've tried, I can't get the sort to apply to the initial fetch.
Any ideas at all?

As you have probably seen at one point or another, if the predicate you pass into your NSFetchRequest is a block predicate, your fetch request will fail. This happens because CoreData needs to translate your predicate into SQL so it can run it against the database. The result is that a predicate is never actually compared against the objects that result from the fetch request.
The same is true of sort descriptors. The fetch request does not perform the comparison against the objects when the fetch is performed. It is passed as part of the SQL.
You also describe an interesting exception to the rules I describe above. When you have an existing NSFetchedResultsController and add an object to the NSManagedObjectContext, the NSFetchedResultsController is updated by evaluating the NSPredicate and NSSortDescriptor against the object, rather than converting them to SQL.

Related

iPhone how to use NSPredicate to filter Core Data by the parent entity?

My core data is defined as this:
user has many events;
event has a single user relationship;
Both user and event are core data entities. The user entity is passed in through a storyboard segue.
I'm trying to configure NSPredicate to populate the detail UITableView for that user with only events for that particular user.
So far I have tried
//does not work
NSPredicate* onlyThisUserPredicate = [NSPredicate predicateWithFormat:#"user == %#",self.appUser];
//does not work
NSPredicate* onlyThisUserPredicate = [NSPredicate predicateWithFormat:#"SELF.user == %#",self.appUser];
What is the proper syntax to compare events and only return those that have user object equal to the specified user object?
UPDATE:
I'm trying to be able to add events to the user with this kind of fetched results controller:
-(NSFetchedResultsController*)fetchedResultsController
{
if (__fetchedResultsController != nil) {
return __fetchedResultsController;
}
// Set up the fetched results controller.
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:[Event managedObjectContext]];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
//I need to configure this user
NSPredicate* onlyThisUserPredicate = [NSPredicate predicateWithFormat:#"user = %#",self.appUser];
// The first sort key must match the section name key path key if present, otherwise the initial dataset would be messed up: rows in incorrect sections
NSString* firstSortKey = #"createDate";
NSSortDescriptor *firstSortDescriptor = [[NSSortDescriptor alloc] initWithKey:firstSortKey ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:firstSortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setPredicate:onlyThisUserPredicate];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[Event managedObjectContext] sectionNameKeyPath:nil cacheName:#"Events"];
self.fetchedResultsController = aFetchedResultsController;
aFetchedResultsController.delegate = self;
// [aFetchedResultsController release];
[sortDescriptors release];
[fetchRequest release];
NSError *error = nil;
if (![__fetchedResultsController performFetch:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
// abort();
}
return __fetchedResultsController;
}
Thank you!
OK, there are a couple of things that I can think of that might cause this behavior.
First, have you validated the value of self.appUser within this function? Is it set to what you expect?
Second, have you made sure your headers are all up to date and included in this file? Sometimes I've experienced odd behavior when my headers aren't up to date with the coredata model.
So this predicate is for the User entity correct? If so, did you try this:
NSPredicate* onlyThisUserPredicate = [NSPredicate predicateWithFormat:#"SELF == %#",self.appUser];
Then you could access your events through:
[self.appUser events];
If you've already retrieved the 'user' from the Core Data store, then you should be able to access its events simply by following that relationship -- no need to do a separate fetch request:
NSSet *events = self.appUser.events;
On the other hand, if self.appUser isn't a managed object, then using the == operator in your predicate is probably the problem. So let me assume that self.appUser is just a string containing the name of the user, not the user object from the data store. Then you'd use the 'like' operator in your predicate:
NSPredicate* onlyThisUserPredicate = [NSPredicate predicateWithFormat:#"user like %#",self.appUser];
Also, be sure that you've specified the right entity in your fetch request. For what you've described, you should be doing the fetch with the entity description for your event entity.

How can I use NSFetchedResultsController to fetch results based on an entity's relationship?

I have 2 views that contain a UITableView each. They are both displayed at the same time, side by side, on an iPad.
I am using Core Data for all data. Both tables need to be edited (rows added, deleted, etc), so I'd like to use a NSFetchedResultsController in each view to handle all this for me.
The contents of the second table depend on what is selected in the first table. So, when selecting an item in the first table, that object is passed to the view with the second table (so I do already have access to the data that should go into the second table), but I'd like to try to use all the built-in handling of the NSFRC if possible.
The model is along the lines of: University (uniID, uniName, students) and Student (stuID, stuName, university). So the relationship is: University <-->> Student.
I'm using the following code in the NSFRC, but it's returning 0 results:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSManagedObjectContext *context = appDelegate.managedObjectContext;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Student" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]initWithKey:#"stuName" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"university == %#",self.selectedUniversity];
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
fetchedResultsController.delegate = self;
[sort release];
[fetchRequest release];
[theFetchedResultsController release];
return fetchedResultsController;
}
I would be most grateful if someone could at least point me in the right direction...
Have you remembered to performFetch: ?
i.e.
NSError *error;
BOOL success = [fetchedResultsController performFetch:&error];
Ok, so I solved the issue. The NSFRC wasn't being updated when the predicate term needed to change (i.e. a University had been selected), of course, because of:
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
Having the NSFRC recreated every time it's called (by removing the above code) doesn't work either, because the fetch needs to be executed after it's been created, which can't happen just before numberOfRowsInSection is called (as this method calls, and therefore recreates, the NSFRC).
So, I added a BOOL to the view called newFetchRequired which is set to YES every time a new University is selected. In the NSFRC, the above code should be changed to:
if (fetchedResultsController != nil && !newFetchRequired) {
return fetchedResultsController;
}
newFetchRequired = NO;
The fetch is then performed correctly (which calls and recreates the NSFRC):
[self.fetchedResultsController performFetch:&error];
I hope this helps anyone in a similar situation.
Thanks to Ashley for the alternative suggestion.
I don't know if you're still checking this and I'm not quite sure if I understood your question correctly.. but why do you create the NSFRC from scratch when you just want to change the predicate? Here's how I would do it:
When a new University is selected just add in the code right there:
NSFetchRequest *fetchRequest = [self.fetchedResultsController fetchRequest];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"university == %#",self.selectedUniversity];
[fetchRequest setPredicate:predicate];
NSError *error;
[fetchedResultsController performFetch:&error];
and you're done. (Sorry if this is totally not what you were looking for).
edit: You could also simply write this into a private function and call it every time a university is selected.

Core Data: Fetch all entities in a to-many-relationship of a particular object?

in my iPhone application I am using simple Core Data Model with two entities (Item and Property):
Item
name
properties
Property
name
value
item
Item has one attribute (name) and one one-to-many-relationship (properties). Its inverse relationship is item. Property has two attributes the according inverse relationship.
Now I want to show my data in table views on two levels. The first one lists all items; when one row is selected, a new UITableViewController is pushed onto my UINavigationController's stack. The new UITableView is supposed to show all properties (i.e. their names) of the selected item.
To achieve this, I use a NSFetchedResultsController stored in an instance variable. On the first level, everything works fine when setting up the NSFetchedResultsController like this:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all item objects.
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetch setFetchBatchSize:10];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
However, on the second-level UITableView, I seem to do something wrong. I implemented the fetchedresultsController in a similar way:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all property objects that belong to the previously selected item
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
// fetch all Property entities.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Property" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
// limit to those entities that belong to the particular item
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"item.name like '%#'",self.item.name]];
[fetch setPredicate:predicate];
// sort it. Boring.
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
NSError *error = nil;
NSLog(#"%d entities found.",[self.moContext countForFetchRequest:fetch error:&error]);
// logs "3 entities found."; I added those properties before. See below for my saving "problem".
if (error) NSLog("%#",error);
// no error, thus nothing logged.
[fetch setFetchBatchSize:20];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
Now it's getting weird. The above NSLog statement returns me the correct number of properties for the selected item. However, the UITableViewDelegate method tells me that there are no properties:
-(NSInteger) tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSLog(#"Found %d properties for item \"%#\". Should have found %d.",[sectionInfo numberOfObjects], self.item.name, [self.item.properties count]);
// logs "Found 0 properties for item "item". Should have found 3."
return [sectionInfo numberOfObjects];
}
The same implementation works fine on the first level.
It's getting even weirder. I implemented some kind of UI to add properties. I create a new Property instance via Property *p = [NSEntityDescription insertNewObjectForEntityForName:#"Property" inManagedObjectContext:self.moContext];, set up the relationships and call [self.moContext save:&error]. This seems to work, as error is still nil and the object gets saved (I can see the number of properties when logging the Item instance, see above). However, the delegate methods are not fired. This seems to me due to the possibly messed up fetchRequest(Controller).
Any ideas? Did I mess up the second fetch request? Is this the right way to fetch all entities in a to-many-relationship for a particular instance at all?
You need to actually perform the fetch for the table view controller:
// ...create the fetch results controller...
NSError *fetchRequestError;
BOOL success = [fetchedResultsController performFetch:&fetchRequestError];

coredata sqlite malformed DB

I have problems with my coredata sqlite DB, which hosts a book DB. After a crash a user experiences has the problem, that the data isn't shown properly any more in his/her tableview.
This is due to the fact, that the performFetch method returns an error:
[NSFetchedResultsController deleteCacheWithName:nil];
if (![[self fetchedResultsController] performFetch:&error]) {
//NSArray *array = [[self fetchedResultsController] fetchedObjects];
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
#ifdef DEBUG
abort();
#endif
}
which results in this error message:
Unresolved error Error Domain=NSCocoaErrorDomain Code=134060 "The operation couldn’t be completed. (Cocoa error 134060.)" UserInfo=0x1dff80 {reason=The fetched object at index 312 has an out of order section name 'Z. Objects must be sorted by section name'}, {
reason = "The fetched object at index 312 has an out of order section name 'Z. Objects must be sorted by section name'";
When I have a look into the sqlite file with 'SQLite Database Browser 2.0 b1' the attributes of each entity seem to be ok.
When I delete some of the entities being mentioned everything works fine again.
I would like to know how I can find out what exactly is wrong with the mentioned entities and fix that, so the user can use his/her data again. Of course I want to fix the bug which causes the malformed DB as well but that is out of focus in this post.
Does anybody have any hints where I could have a look at or what might be malformed within my DB or what "an out of order section name" is?
This is the code for my fetchedResultsController:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity;
entity = [NSEntityDescription entityForName:#"Book" inManagedObjectContext:[[GlobalData sharedInstance] managedObjectContext]];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:10];
//set searchPredicate
NSPredicate *predicate = nil;
if (self.bibList != nil) {
predicate = [NSPredicate predicateWithFormat:#"ANY BibLists.name LIKE %#", self.bibList.name];
}
if (predicate) {
[fetchRequest setPredicate:predicate];
}
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor;
NSString *sortDescriptorString = nil;
sortDescriptorString = #"title";
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortDescriptorString ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSString *sectionNameKeyPath = nil;
sectionNameKeyPath = #"uppercaseFirstLetterOfTitle";
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController;
aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:[[GlobalData sharedInstance] managedObjectContext]
sectionNameKeyPath:sectionNameKeyPath
cacheName:#"Bibliography"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
Thanks
b00tsy
I think what is going on here is that your SQL store is corrupted and/or you have an unusual sectionNameKeyPath.
The Core Data schema prefixes all the SQL column names with Z so an error of a name of 'Z suggest a corrupted SQL table. To test for that, execute the fetch request directly instead of using the fetched results controller and see if you can fetch all the objects.
If you can't, then the SQL store is corrupted. Most likely, some table component is simply name 'Z instead of something like ZAttributeName. You will have to edit the SQL directly but that is tricky because of the custom private schema. See this post to get an idea what to look for. There are not any tools for doing this because corruption of the store is so rare.
If the fetch works then the problem is with the sectionNameKeyPath being handed to the fetched results controller. Right now, it looks like you have an entity attribute with the first letter of the title attribute. (This is redundant because by default, the FRC will automatically return alphabetic sections based on any string attribute.) Try changing the sectionNameKeyPath to just the title attribute name (most likely "title"). In any case, just dispense with the uppercaseFirstLetterOfTitle attribute altogether.
The following statement in the documentation hints at the error.
If this key path is not the same as that specified by the first sort descriptor in fetchRequest, they must generate the same relative orderings.
It seems that the item at index 312 is not in the correct section when sorting on title alone. This could be a result of the title property beginning with a non-letter or lowercase letter, which using lexical sorting would put it after the Z section but the value of the item's uppercaseFirstLetterOfTitle is not 'Z'. As a suggestion, try adding a sort descriptor on the uppercaseFirstLetterOfTitle as the first sort descriptor, then add the sort descriptor on title.

NSFetchedResultsController with sections created by first letter of a string

Learning Core Data on the iPhone. There seem to be few examples on Core Data populating a table view with sections. The CoreDataBooks example uses sections, but they're generated from full strings within the model. I want to organize the Core Data table into sections by the first letter of a last name, a la the Address Book.
I could go in and create another attribute, i.e. a single letter, for each person in order to act as the section division, but this seems kludgy.
Here's what I'm starting with ... the trick seems to be fooling the sectionNameKeyPath:
- (NSFetchedResultsController *)fetchedResultsController {
//.........SOME STUFF DELETED
// Edit the sort key as appropriate.
NSSortDescriptor *orderDescriptor = [[NSSortDescriptor alloc] initWithKey:#"personName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:orderDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"personName" cacheName:#"Root"];
//....
}
Dave DeLong's approach is good, at least in my case, as long as you omit a couple of things. Here's how it's working for me:
Add a new optional string attribute
to the entity called
"lastNameInitial" (or something to
that effect).
Make this property transient. This
means that Core Data won't bother
saving it into your data file. This
property will only exist in memory,
when you need it.
Generate the class files for this
entity.
Don't worry about a setter for this
property. Create this getter (this is
half the magic, IMHO)
// THIS ATTRIBUTE GETTER GOES IN YOUR OBJECT MODEL
- (NSString *) committeeNameInitial {
[self willAccessValueForKey:#"committeeNameInitial"];
NSString * initial = [[self committeeName] substringToIndex:1];
[self didAccessValueForKey:#"committeeNameInitial"];
return initial;
}
// THIS GOES IN YOUR fetchedResultsController: METHOD
// Edit the sort key as appropriate.
NSSortDescriptor *nameInitialSortOrder = [[NSSortDescriptor alloc]
initWithKey:#"committeeName" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:nameInitialSortOrder]];
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"committeeNameInitial" cacheName:#"Root"];
PREVIOUSLY: Following Dave's initial steps to the letter generated issues where it dies upon setPropertiesToFetch with an invalid argument exception. I've logged the code and the debugging information below:
NSDictionary * entityProperties = [entity propertiesByName];
NSPropertyDescription * nameInitialProperty = [entityProperties objectForKey:#"committeeNameInitial"];
NSArray * tempPropertyArray = [NSArray arrayWithObject:nameInitialProperty];
// NSARRAY * tempPropertyArray RETURNS:
// <CFArray 0xf54090 [0x30307a00]>{type = immutable, count = 1, values = (
// 0 : (<NSAttributeDescription: 0xf2df80>),
// name committeeNameInitial, isOptional 1, isTransient 1,
// entity CommitteeObj, renamingIdentifier committeeNameInitial,
// validation predicates (), warnings (), versionHashModifier (null),
// attributeType 700 , attributeValueClassName NSString, defaultValue (null)
// )}
// NSInvalidArgumentException AT THIS LINE vvvv
[fetchRequest setPropertiesToFetch:tempPropertyArray];
// *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
// reason: 'Invalid property (<NSAttributeDescription: 0xf2dfb0>),
// name committeeNameInitial, isOptional 1, isTransient 1, entity CommitteeObj,
// renamingIdentifier committeeNameInitial,
// validation predicates (), warnings (),
// versionHashModifier (null),
// attributeType 700 , attributeValueClassName NSString,
// defaultValue (null) passed to setPropertiesToFetch: (property is transient)'
[fetchRequest setReturnsDistinctResults:YES];
NSSortDescriptor * nameInitialSortOrder = [[[NSSortDescriptor alloc]
initWithKey:#"committeeNameInitial" ascending:YES] autorelease];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:nameInitialSortOrder]];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"committeeNameInitial" cacheName:#"Root"];
I think I've got yet another option, this one uses a category on NSString...
#implementation NSString (FetchedGroupByString)
- (NSString *)stringGroupByFirstInitial {
if (!self.length || self.length == 1)
return self;
return [self substringToIndex:1];
}
#end
Now a little bit later on, while constructing your FRC:
- (NSFetchedResultsController *)newFRC {
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest:awesomeRequest
managedObjectContext:coolManagedObjectContext
sectionNameKeyPath:#"lastName.stringGroupByFirstInitial"
cacheName:#"CoolCat"];
return frc;
}
This is now my favorite approach. Much cleaner/easier to implement. Moreover, you don't have to make any changes to your object model class to support it. This means that it'll work on any object model, provided the section name points to a property based on NSString
Here's how you might get it to work:
Add a new optional string attribute to the entity called "lastNameInitial" (or something to that effect).
Make this property transient. This means that Core Data won't bother saving it into your data file. This property will only exist in memory, when you need it.
Generate the class files for this entity.
Don't worry about a setter for this property. Create this getter (this is half the magic, IMHO)
- (NSString *) lastNameInitial {
[self willAccessValueForKey:#"lastNameInitial"];
NSString * initial = [[self lastName] substringToIndex:1];
[self didAccessValueForKey:#"lastNameInitial"];
return initial;
}
In your fetch request, request ONLY this PropertyDescription, like so (this is another quarter of the magic):
NSDictionary * entityProperties = [myEntityDescription propertiesByName];
NSPropertyDescription * lastNameInitialProperty = [entityProperties objectForKey:#"lastNameInitial"];
[fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:lastNameInitialProperty]];
Make sure your fetch request ONLY returns distinct results (this is the last quarter of the magic):
[fetchRequest setReturnsDistinctResults:YES];
Order your results by this letter:
NSSortDescriptor * lastNameInitialSortOrder = [[[NSSortDescriptor alloc] initWithKey:#"lastNameInitial" ascending:YES] autorelease];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:lastNameInitialSortOrder]];
execute the request, and see what it gives you.
If I understand how this works, then I'm guessing it will return an array of NSManagedObjects, each of which only has the lastNameInitial property loaded into memory, and who are a set of distinct last name initials.
Good luck, and report back on how this works. I just made this up off the top of my head and want to know if this works. =)
I like Greg Combs answer above. I've made a slight modification so that strings like "Smith" and "smith" can appear in the same section by converting the strings to upper case:
- (NSString *)stringGroupByFirstInitial {
NSString *temp = [self uppercaseString];
if (!temp.length || temp.length == 1)
return self;
return [temp substringToIndex:1];
}
I encounter this issue all the time. The solution that seems best that i always come back to is to just give the entity a real first initial property. Being a real field provides for more efficient searching and ordering as you can set the field to indexed. It doesn't seem like it's too much work to pull the first initial out and populate a second field with it when the data is first imported / created. You have to write that initial parsing code either way, but you could do it once per entity and never again. The drawbacks seem to be you are storing one extra character per entity (and the indexing) really, that's likely insignificant.
One extra note. I shy away from modifying the generated entity code. Maybe i'm missing something, but the tools for generating CoreData entities do not respect any code i might have put in there. Either option i pick when generating the code removes any customizations i might have made. If i fill up my entities with clever little functions, then i need to add a bunch of properties to that entity, i can't regenerate it easily.
swift 3
first, create extension to NSString (because CoreData is using basically NSString)
extension NSString{
func firstChar() -> String{
if self.length == 0{
return ""
}
return self.substring(to: 1)
}
}
Then sort using firstChar keypath, in my case, lastname.firstChar
request.sortDescriptors = [
NSSortDescriptor(key: "lastname.firstChar", ascending: true),
NSSortDescriptor(key: "lastname", ascending: true),
NSSortDescriptor(key: "firstname", ascending: true)
]
And Finally
Use the firstChar keypath for sectionNameKeyPath
let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "lastname.firstChar", cacheName: "your_cache_name")
I think I have a better way to do this. Instead of using transient property, in view will appear. Recalculate the derived property of the NSManagedObject and save the context.After the changes you can just reload the table view.
Here is an example of calculating the number of edges of each vertex, then sort the vertexes by the number of the edges. In this example, Capsid is vertex, touch is edge.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
[self.tableView reloadData];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Capsid"];
NSError *error = nil;
NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
if (error) {
NSLog(#"refresh error");
abort();
}
for (Capsid *capsid in results) {
unsigned long long sum = 0;
for (Touch *touch in capsid.vs) {
sum += touch.count.unsignedLongLongValue;
}
for (Touch *touch in capsid.us) {
sum += touch.count.unsignedLongLongValue;
}
capsid.sum = [NSNumber numberWithUnsignedLongLong:sum];
}
if (![self.managedObjectContext save:&error]) {
NSLog(#"save error");
abort();
}
}
- (NSFetchedResultsController *)fetchedResultsController
{
if (__fetchedResultsController != nil) {
return __fetchedResultsController;
}
// Set up the fetched results controller.
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Capsid" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
// NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"timeStamp" ascending:NO];
// NSSortDescriptor *sumSortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"sum" ascending:NO];
// NSArray *sortDescriptors = [NSArray arrayWithObjects:sumSortDescriptor, nil];
[fetchRequest setReturnsDistinctResults:YES];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"sum" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return __fetchedResultsController;
}