views:

4527

answers:

4

Learning Core Data on the iPhone... There seems to be few examples on Core Data tables 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, ala 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"];
//....
}
+2  A: 

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. =)

Dave DeLong
This certainly sounds promising, thank you so much! I'll let you know how it works, as I imagine others will face this same issue soon enough.
Greg Combs
My reply is truncated and garbled, please see my followup "answer"...
Greg Combs
[Update] It looks like you may be more right than wrong. If I just use the getter-driven attribute in the standard example code, without all the property setting business, I get the proper number of sections.
Greg Combs
@Greg cool! I wasn't sure if the PropertyDescription was necessary, but I thought it might be.
Dave DeLong
I'm wondering what the impact is on performance though, if you have many many records. With N records, I think that this method would have to make N queries into the backing store, whereas if you used a "real" key path it might be able to do it in only a single query.
sbwoodside
@sbwoodside I don't know. I'm using it with 181 records (tableview cells) and it does fine. But I don't know what would happen if you had to do this thousands of times. I suspect if that were the case, you'd want to work up a proper dictionary or something. I was more aimed at simplicity and clarity, since I don't have that many records anyway.
Greg Combs
+7  A: 

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"];
Greg Combs
Major kudos - using 'committeeName' for the sort descriptor vs 'committeeNameInitial' for the sectionNameKeyPath was a huge help.
Luther Baker
A: 

i tried to compile the code reported above, but i get the same error notified by greg. for each item in my db it prints on the console that lastNameInitial is null. any suggestions?

eric
Which code are you attempting? You should use the first section in the "checked" answer. It begins with: // THIS ATTRIBUTE GETTER GOES IN YOUR OBJECT MODELGive that one a shot and see how it works for you. Let me know.
Greg Combs
i tried it, but it doesn't work. i have put a breakpoint in the getter method, but the program never stops, so it never enters in it. In my entity.h file i've defined lastNameInitial as NSString* and in the .m file i set @dynamic lastNameInitial. Is it wrong?
eric
A: 

hi everyone.

I tried to do that for a similar case. But there's an issue. You can't use a transient property to init a NSSortDescriptor. For example if you set the NSSortDescriptor with this "committeeNameInitial" it should not work. So you set "committeeName", here no problem with the sort because the result will be strictly the same.

In my case (a URL) if I want to sort with the text after the @"http://" I don't see how to do it with my transient entry. And actually I don't want to update my store to set it in a non transient state, because the data will be just redundant.

Maybe there's something to do with :

NSSortDescriptor *sortD = [[NSSortDescriptor alloc] initWithKey:@"URL" ascending:TRUE selector:@selector(compareDomain:)];

and redo the job of splitting my strings in the compareDomain method ... ?

If someone has an advice, please share the knowledge, thanks ;)

PS : I say that you cant put a transient property on a NSSortDescriptor because it's said on the official documentation from apple.

adrian Coyle