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 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:
Leaf both of which inherit from
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,
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
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
@"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
Points To Note
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
Group entities (try them in the sample project!), overriding these is usually necessary, which is what’s done with the
The project also includes a bunch of categories that make life so much easier when working with
Write the index paths of the dragged items to the pasteboard as
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.
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.
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)
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)
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]];
[self setSelectedNode:[self treeNodeForObject:object]];
- (NSIndexPath *)indexPathToObject:(id)object;
return [[self treeNodeForObject:object] indexPath];