NSFetchedResultsControllerDelegate methods not being called if its predicate use IN? - iphone

I'm implementing an UITableViewController in conjunction with NSFetchedResultsController.
When the UITableViewController is instantiated the NSFetchedResultsController is constructed (substantially in the same way as CoreDataBooks example) with a different predicate, based on selection the user made on the previous controller.
Sometimes I set the predicate in this way:
predicate = [NSPredicate predicateWithFormat:#"container = %#", aManagedObject];
sometimes in this way:
predicate = [NSPredicate predicateWithFormat:#"container IN (%#)", aNSMutableSetOfObjectsID];
Both predicates works fine because they fetch correctly all my objects as expected.
The problem occur when I try to insert a new managed object into the list when the second predicate is setted. Infact in this case the FRC delegate methods are never called, so the tableview is not automatically updated.
I'm sure the new object is correctly added to the same context used by the FRC, and that its "container" relation is set so that the delegate should be triggered, but it doesn't!
What I have tried:
I say that the second predicate is working fine because if I add a new object and then pop the UITableViewController and push it again, forcing all data to be refetched, the new object appear in the list without problem.
Another test I made is to force the FRC to be recalculated immediatelly after the new object is added:
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
exit(-1);
}
[self.tableView reloadData];
the new object appear correctly.
Code:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController == nil) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *managedObjectContext = realEstate.managedObjectContext;
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Item" inManagedObjectContext:managedObjectContext]];
[fetchRequest setPredicate:[self getBasePredicate]];
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name"
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:nameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[nameDescriptor release];
[sortDescriptors release];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"sectionIndex"
cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
[aFetchedResultsController release];
[fetchRequest release];
}
return fetchedResultsController;
}
// return a predicate for the fetchedResultsController
- (NSPredicate *)getBasePredicate {
NSPredicate *predicate = nil;
if ([[managedObject valueForKey:#"subcontainersCount"] intValue] == 0)
// container without sons **(FIRST PREDICATE)**
predicate = [NSPredicate predicateWithFormat:#"container = %#", managedObject];
else {
// container with sons **(SECOND PREDICATE)**
predicate = [NSPredicate predicateWithFormat:#"container IN (%#)", [Container allDescendantsObjectID:(Container *)managedObject includeSource:YES]];
}
return predicate;
}
- (void)addNewItem {
Item *newItem = (Item *)[NSEntityDescription insertNewObjectForEntityForName:#"Item"
inManagedObjectContext:realEstate.managedObjectContext];
newItem.name = #"my new item";
newItem.container = aContainerManagedObject;
NSError *error = nil;
if (![realEstate.managedObjectContext save:&error]) {
abort();
}
}
The NSFetchedResultsControllerDelegate methods I'm using are taken from the "More iPhone 3 Development". The complete code can be see here (pages 30-31, 35).
I can post here if needed.

Solved replacing the objects ID with NSManagedObject into the predicate.
Before: [NSPredicate predicateWithFormat:#"container IN (%#)", aNSMutableSetOfObjectsID];
After: [NSPredicate predicateWithFormat:#"container IN (%#)", aNSMutableSetOfNSManagedObject];
For some reasons the FRC delegate is not called if predicate fetch objects using ID instead of exact NSManagedObjects.
I'm testing on iPhoneOS 3.1.3 and 3.2. However iPhoneOS 4.0 should support both objectsID and NSManagedObject and correctly call the FRC delegate methods, but I can't test on 4.0 for now.

Related

Core Data to-many relationship get data

In my code i want to create tableView with List sections. I use scheme like this one:
I use NSFetchResultController which i define in this way:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Item" inManagedObjectContext:coreDataController.masterManagedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"addedAt" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"itemIsChecked = 1"];
[fetchRequest setPredicate:predicate];
[fetchRequest setResultType:NSDictionaryResultType];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:coreDataController.masterManagedObjectContext sectionNameKeyPath:#"toList.listName"
cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
Now in cellForRowAtIndexPath: i want to get data form my fetchResultController, so i do this in way:
Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
and then if i want to access one of the item's field (for example itemText), it crash:
NSLog(#"item itemtext = %#", item.itemText);
with error:
-[NSKnownKeysDictionary1 itemText]: unrecognized selector sent to instance 0x1215fd90
What i do wrong in my code?
You have set
[fetchRequest setResultType:NSDictionaryResultType];
and therefore the fetched results controller returns NSDictionary objects, not Item objects. So your element
Item *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
is a NSDictionary, not an Item. Since dictionaries do not have a itemText method, item.itemText crashes. You could retrieve the value from the dictionary with
NSDictionary *item = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"item itemtext = %#", [item objectForKey:#"itemText"]);
But if you don't have a specific reason to set the result type to NSDictionaryResultType, you should just delete that line. Change tracking of the fetched results controller (i.e. automatic table view updates) do not work with resultType == NSDictionaryResultType.
Note also that if you have set a sectionNameKeyPath, then you must add a sort descriptor with the same key path "toList.listName" and use it as first sort descriptor for the fetch request.
unrecognized selector sent to instance generally occurs due to bad memory management. Check if you are trying to point an object which was released earlier. Also check for IBOutlet connection in xib for lable itemText.

NSFetchedResultsController custom sort not getting called

I am currently trying to populate a UITableView in my project from Core Data using NSFetchedResultsController. I am using a custom search with a comparator (although I have also tried a selector and had an identical problem):
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:#"Object" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"objectName" ascending:YES comparator:^(id s1, id s2) {
NSLog(#"Comparator");
//custom compare here with print statement
}];
NSLog(#"Sort Descriptor Set");
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Object" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"firstLetterOfObject" cacheName:#"Objects"];
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
if (![fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController;
When I enter this tab, I have logged all over the program and found that the NSFetchedResultsController does not even enter the comparator block when fetching. It instead sorts it with some default sorting method.
If I delete and add an Object with an objectName, however, it then does enter the comparator block and correctly sort the table.
Why does the NSFetchedResultsController not sort using the comparator until the managed object model is changed?
Notes: I have tried also turning off caching, and/or performing a fetch in viewDidLoad:, but it seems that how many times I fetch does not matter, but when. For some reason it only uses my sorting after the object model has been changed.
There are a couple of things I can think of. First, though this may not be your problem, you cannot sort on transient properties. But more likely is that when sorting in a model backed by a SQL store, the comparator gets "compiled" to a SQL query, and not all Objective-C functions are available. In this case, you'd need to sort in memory after the fetch is performed.
EDIT: See this doc, specifically the Fetch Predicates and Sort Descriptors section.
I see the same problem and a way to work around it is to modify an object, save the change then restore it to its original value and save again.
// try to force an update for correct initial sorting bug
NSInteger count = [self.fetchedResultsController.sections count];
if (count > 0) {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:0];
count = [sectionInfo numberOfObjects];
if (count > 0) {
NSManagedObject *obj = [self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
NSString *name = [obj valueForKey:#"name"];
[obj setValue:#"X" forKey:#"name"];
// Save the context.
[self saveContext];
[obj setValue:name forKey:#"name"];
// Save the context.
[self saveContext];
}
}
Sorry, but did you miss the final fetch part to your code snippet?:
NSError *error;
BOOL success = [aFetchedResultsController performFetch:&error];
Don't forget to release the request too:
[fetchRequest release];

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];

Dealloc'd Predicate crashing iPhone App!

To preface, this is a follow up to an inquiry made a few days ago:
https://stackoverflow.com/questions/2981803/iphone-app-crashes-when-merging-managed-object-contexts
Short Version: EXC_BAD_ACCESS is crashing my app, and zombie-mode revealed the culprit to be my predicate embedded within the fetch request embedded in my Fetched Results Controller. How does an object within an object get released without an explicit command to do so?
Long Version:
Application Structure
Platforms View Controller -> Games View Controller (Predicated upon platform selection) -> Add Game View Controller
When a row gets clicked on the Platforms view, it sets an instance variable in Games View for that platform, then the Games Fetched Results Controller builds a fetch request in the normal way:
- (NSFetchedResultsController *)fetchedResultsController{
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
//build the fetch request for Games
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Game"
inManagedObjectContext:context];
[request setEntity:entity];
//predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"platform == %#",
selectedPlatform];
[request setPredicate:predicate];
//sort based on name
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name"
ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
//fetch and build fetched results controller
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:nil
cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[sortDescriptor release];
[sortDescriptors release];
[predicate release];
[request release];
[aFetchedResultsController release];
return fetchedResultsController;
}
At the end of this method, the fetchedResultsController's _fetch_request -> _predicate member is set to an NSComparisonPredicate object. All is well in the world.
By the time - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section gets called, the _predicate is now a Zombie, which will eventually crash the application when the table attempts to update itself.
I'm more or less flummoxed. I'm not releasing the fetched results controller or any of it's parts, and the only part getting dealloc'd is the predicate. Any ideas?
EDIT:
As a test, I added this line to the Fetched Results Controller method:
[fetchedResultsController.fetchRequest.predicate retain];
And now it doesn't crash, but that seems like a patch, not something I should be doing.
You shouldn't be releasing your predicate variable. You didn't invoke new, alloc, retain, or copy (This is the "narc" rule) to create the predicate, so you are not responsible for releasing it. That's where your zombie is coming from.

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;
}