NSTreeController and Core Data, Sorted.

Having recently taken the plunge into Core Data I decided it was time to rip out all the model code from my current application and replace it with a Core Data version. After about a day I had my app up and running again but with one huge problem, the content of my NSOutlineView always appeared in a random order. Such is the problem with Core Data that NSManagedObjects store their to-many relationships in an NSSet, not an NSArray, which is unordered. So when your NSTreeController tries to display its data it appears in a random order.

This is not nice, imagine if the playlists in your iTunes library always changed their order? It gets even worse if your user wants to use drag and drop. In this case they decide the order, and they’d probably want it to stay that way.

The question of how to do this comes up so many times, along with the question of how to use multiple classes for nodes in the tree. Well, here’s your answer, and I’ve made a sample Xcode project to demonstrate. (alt link)

The Model
Multiple-entity model that can be used in an NSTreeController
The model I’ve set up has a single abstract entity called TreeNode which has a boolean isLeaf attribute, an NSString *displayName, and an NSNumber *sortIndex (which is the one of the main reasons I’m writing this). It also has a to-many children relationship that has an inverse to-one parent relationship. Then there are two other entities: Group and Leaf both of which inherit from TreeNode. The Group entity has a few other boolean attributes that make writing an NSOutlineView delegate class really simple. The Leaf doesn’t have any of its own attributes yet, that’s for you guys to ponder.

The Tree Controller
The tree controller is our custom ESTreeController class. It is set up in the nib and has only 2 bindings, those of the @"managedObjectContext" and the @"sortDescriptors". It’s (obviously) operating in entity mode which is set to our abstract entity of TreeNode, and the children and leaf keypaths are set to attributes of the model: the oh-so-inventively named, children and isLeaf.

The Outline View
The outline view is also a subclass of NSOutlineView, but only to support expanded-state saving. The NSTableColumn in the view has only a single binding: the @"value" binding is bound to the tree controller’s @"arrangedObjects.displayName".

The rest of the UI only exists for demonstration purposes (so yeah its ugly), so we can see which ESTreeController method is invoked by the class’ standard actions.

The Sort Index
The @"sortIndex" attribute is the key to keeping the tree sorted, and it’s persistent allowing the sort to be maintained across sessions. It is simply an unsigned integer. Not having to apply a unique value to each node in the tree makes this a whole lot easier, nodes only require to have a unique number that defines their location within their own group. All we have to do is keep the sort index as the last index of the corresponding NSTreeNode’s index path.

There are 3 places this must be updated: on insertion, removal and movement. Functionality for this is provided by a single method -updateSortOrderOfModelObjects, which takes the last index of the tree node’s index path and sets it as the sort index of the representedObject. Simple. We make sure this is done correctly by overriding:

- (void)insertObject:(id)object atArrangedObjectIndexPath:(NSIndexPath *)indexPath;
- (void)insertObjects:(NSArray *)objects atArrangedObjectIndexPaths:(NSArray *)indexPaths;
- (void)removeObjectAtArrangedObjectIndexPath:(NSIndexPath *)indexPath;
- (void)removeObjectsAtArrangedObjectIndexPaths:(NSArray *)indexPaths;
- (void)moveNode:(NSTreeNode *)node toIndexPath:(NSIndexPath *)indexPath;
- (void)moveNodes:(NSArray *)nodes toIndexPath:(NSIndexPath *)startingIndexPath;

The tree controller is then bound to an NSSortDescriptor with keypath @"sortIndex".

Points To Note
NSTreeController’s -add:, -addChild:, -insert:, -insertChild: and remove: all call the plural forms of the above methods, and proceed to try and add a TreeNode entity to the tree. This can cause problems in the outline view’s delegate as this expects either Leaf or Group entities (try them in the sample project!), overriding these is usually necessary, which is what’s done with the -newLeaf: and -newGroup: actions.

The project also includes a bunch of categories that make life so much easier when working with NSIndexPath, NSTreeController and NSTreeNode.

Drag and Drop

-outlineView:writeItems: toPasteboard:
Write the index paths of the dragged items to the pasteboard as NSData objects.

-outlineView:validateDrop:proposedItem:proposedChildIndex:
Determine if the drop location is valid, not allowing drops on a leaf node (for the case of iTunes playlists being allowed to do this wouldn’t make sense), or if one of the dragged nodes is a group and the proposed location is on of its own descendants.

-outlineView:acceptDrop:item:childIndex:
Accept the drop and move the nodes for the dragged index paths. The edge case is is when dropping on the root of the tree. In this case the proposed parent of the drop location is nil and generating the index path for insertion therefore returns nil. We check for a nil parent and create a blank NSIndexPath if this is the case.

Summary
With the (relatively small) amount of code we have an NSOutlineView powered by Core Data and NSTreeController with some great features:
1) Drag and drop!
2) Persistent state saving!
3) Multiple entities in the tree (extend ad infinitum)
4) Sorting!
5) Some indispensable categories that make these classes so much easier to use (-treeNodeForObject: is great).

Download the Xcode project (requires Mac OS X 10.5)

Update
I’ve realised that there is some repeated code in the extensions, for my own work I’ve replaced the implementation of -flattenedContent with this:
- (NSArray *)flattenedContent;
{
return [[self flattenedNodes] valueForKey:@"representedObject"];
}

There are also these I’ve found useful:
- (void)setSelectedNode:(NSTreeNode *)node;
{
[self setSelectionIndexPath:[node indexPath]];
}

- (void)setSelectedObject:(id)object;
{
[self setSelectedNode:[self treeNodeForObject:object]];
}

- (NSIndexPath *)indexPathToObject:(id)object;
{
return [[self treeNodeForObject:object] indexPath];
}