Amazon.com Widgets

shanecrawford.org Home Grown in Austin

Posted
29 January 2008 @ 10pm

Tagged
Development, Mac

Sorting a CoreData backed NSArrayController

Core Data NSArrayControllerNSArrayController is an incredible way to provide data to many of Cocoa’s UI controls. Typically, it manages an array of data objects which can be sorted, filtered, selected, and basically served up to the UI control for display. Pair this flexibility with the power of CoreData and you have a powerful dynamic.

However, this flexibility starts to break down when you have a user defined list of objects. A typical use case for this scenario would be when using a source list such as the one found in the navigation pane of iTunes. In such a case there is no built in way to define the list order so that Core Data and NSArrayController can sort the data as the user wants it. Instead the data just gets displayed in the natural order as retrieved by CoreData, which may not always be the same and certainly won’t be what the user intended. There is a way around this problem but it isn’t exactly pretty and it undoubtedly is not drag and drop simple. Yes, my friends it’s time to code.

The crux of the issue boils down to sorting. NSArrayController sorts its contents (arrangedObjects) via a defined NSSortDescriptor which will sort your KVO compliant data object based upon a specified key. Witness the awakeFromNib method of our NSArrayController subclass:

- (void)awakeFromNib

{

NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"index"

                                                     ascending:YES];

[self setSortDescriptors:[NSArray arrayWithObject:sort]];

[super awakeFromNib];

}

Of course, this NSSortDescriptor could be set externally to the controller. However, if the illustrated approach is taken then the call to super in the awakeFromNib method in NSArrayController is necessary. Taking a look at the code you will notice that the sort is occurring on a key called ‘index’. This implies that the Core Data object which is being sorted over will need an integer attribute with the name ‘index’ so that the sort can occur. This Core Data object is the one which is managed by NSArrayController and whose order in the controller list is intended to be defined by the end user. You can probably tell by now where this is going. The order defined by the user is set into the ‘index’ attribute.

Reindexing

Whenever a user adds a new object, removes an object or rearranges the objects then the ‘index’ attribute must be updated for all objects managed by the controller. In this way the user defined order is kept. More NSArrayController subclassing:

- (void)remove:(id)sender

{

    [super remove:sender];

    [self reindexEntries];

}- (void)insertObject:(id)object atArrangedObjectIndex:(NSUInteger)index

{

    [object setValue:[NSNumber numberWithInt:index] forKey:@"index"];

    [super insertObject:object atArrangedObjectIndex:index];

    [self reindexEntries];

}

- (void) reindexEntries

{

    // Note: use a temporary array since modifying an item in arrangedObjects

    //       directly will cause the sort to trigger thus throwing off

    //       the re-indexing.

    int count = [[self arrangedObjects] count];

    NSArray *tmpArray = [NSArray arrayWithArray:[self arrangedObjects]];

for(int ndx = 0; ndx < count ; ndx++){

        id entry = [tmpArray objectAtIndex:ndx];

        [entry setValue:[NSNumber numberWithInt:ndx] forKey:@"index"];

    }

}

Not done yet

It seems that this would be it. However, the initial sort upon first run still does not work. At first this issue took a bit of hunting to track down but, after reading the Core Data docs it becomes somewhat obvious. In order to increase performance Core Data will initially read in empty stubs of your data objects. These stubs are known as faults. Whenever an attribute of a fault object is accessed by your code then the entire data for the object is actually retrieved and filled in. This all happens behind the scenes and your code never need know the difference. More can be read on this topic in the Core Data documentation.

It seems that either there is a bug or a gap in my understanding with regards to faults and KVC. NSSortDescriptor will access its defined key attribute for the data object via KVC. From my experience this KVC access does not trigger the fault object to be filled in by Core Data. This leaves the sort to work on empty objects – thus not working the first time the data is displayed. In order to resolve/work around this issue more NSArrayController subclassing is needed:

- (NSArray *)arrangeObjects:(NSArray *)objects

{

    // Note: at this point the data objects are CoreData faults and thus contain

    //       no real data. So, go ahead and batch fault (load) the data for use

    //       in sorting

    NSError *error = nil;

    NSManagedObjectContext *moc = [appDelegate managedObjectContext];

    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"MyEntity"

                                                         inManagedObjectContext:moc];

    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];

    [request setReturnsObjectsAsFaults:NO];

    [request setEntity:entityDescription];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self IN %@", objects];

    [request setPredicate:predicate];

    [moc executeFetchRequest:request error:&error];    NSArray *arranged = [super arrangeObjects:objects];

return arranged;

}

Basically, this code is straight out of the Core Data docs. In order to force the fault data to be loaded we’re retrieving all objects in the controller and forcing the full data to be loaded via the message ‘setReturnsObjectsAsFaults:NO’ (OS X 10.5 only). After implementing this code the initial (and subsequent) sorts work as intended.

That’s it

We’ll that’s it. With a highly subclassed NSArrayController and a modification to your Core Data managed object you should be able to provide user defined order for a specified object. IMO this is a major pain and it would be nice to see some kind of official support for this type of (fairly common) behavior.


4 Comments

Posted by
Matthew Schinckel
7 July 2008 @ 11pm

I was having some issues getting some objects stored in a CoreData store to be sorted, but managed, just using sortDescriptors, to get them sorted.

I don’t know if I did anything else (I may have tweaked something in IB), but it is possible without subclassing the controller.



Posted by
Andrew
24 August 2009 @ 10am

Just wondering if this is still the best way to do this.
It’s over a year old…


Posted by
RvA
7 September 2009 @ 6am

Just wondering if there was a change with snow for this? Also what about when someone has 7000+ objects and has to fill in the faults.


Leave a Comment