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

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

Nice and clearly written article! I have a suggestion though. Now that the sortIndex has become persistent and a part of the model, according to MVC, it means that sortIndex is now owned by the model. It should be the model who keeps the node indexes under control or says which nodes accept new/moved children.
It bothers me that in this implementation the business logic is embedded in the controller. If you modify the model’s sort indexes programmatically, your tree controller does not get to know about it. Also, children can be added or moved programatically and the model can become inconsistent because the decision whether the move is accepted, is made by the NSOutlineView delegate (!) - not the model.
Thanks for the compliment Mark!
I’m afraid I have to disagree with you on this though
but I’d really like for you to explain how your implementation would work though, I’m sure there’s places I can improve this.
Your first point is correct, the sort index is owned by the model, this is essential for the persistence of the sort order. I can’t think of another way of keeping the sorted order between sessions. When doing this, I don’t see why my model objects should have any knowledge of each other at all, that’s why the code to keep the sort indexes in check is in the controller. The controller knows about all of the nodes and the NSTreeNodes. Making the model objects (too) aware of their surroundings seems to break encapsulation, they should just be happy to exist in their own universe (in the larger sense, obviously they can traverse their parent and children relationships).
Your point about the models saying which nodes accept or deny new nodes is fair, but I think the logic that governs which nodes accept new/moved children is forced to be in the NSOutlineView’s delegate due to the way the view works. When you log the proposedIndex when -outlineView:validateDrop….. the outline view returns -1 is the cursor is over a child or at the root of the tree. There’s clearly no way to obtain the NSManagedObject/NSTreeNode that has an indexPath with a -1, so the allowing or denying moves to the delegate.
In my application I have a source list that looks like iTunes but has groups and files like in Xcode. I may have been influenced by my own design requirements, but dropping dragged items onto a file makes no sense and one clearly shouldn’t be able to drop a group on to one of its subgroups. The latter could be done in the model code by would still be mediated in the outline view’s delegate. It would ask the model to validate the drop of the proposed item and the model would have to see if the proposed item was somewhere along the parent relationships. The reason I didn’t do this was for flexibility, by using the model you’re not locking developers in to a certain mindset, there are cases when you would want to be able to rearrange the tree arbitrarily and this model accommodates that.
You’re correct that modifying the sort indexes programmatically doesn’t inform the tree controller, but the resort would take place when NSTreeController’s -rearrangeObjects is called. I would think it quite difficult the modify them programmatically in the first place without messing up the tree. You’d have to decrement and increment the other children around it and, although you can get to the other children using the @”parent.children” keypath, even doing this seems to me to break encapsulation.
You are correct that it could be done, and the model could take care of it all but there would be an immense amount of code to write and some edge cases that may be hard to find. The other confounding factor is that time = money, and getting the tree controller to steal the NSTreeNodes -lastIndex which it has just magically placed in the right place is just too easy to pass up. Although I’m still not convinced that I’ve put things in the wrong place.
The outline view’s delegate methods don’t just allow/deny drops they also return values that the view itself uses to communicate the actions to the user. These methods tell the view to draw its little blue boxes and lines to show where (or not) the drag can go. As the sorting of the tree (in the case of my app) is really a view-related process (its just the ordering the user wants to see) then I think its OK to allow/deny in the delegate, one of the reasons delegates exist is so you can have a third party (almost) that you can ask, “what do you think?”.
Finally, the case of adding and moving children programatically should be done using the tree controller, its whole point is to manage the tree, so if you use ESTreeController’s methods to add/move/remove then you’ll never end up with an inconsistent tree. I think it fit perfectly that the tree controller updates the model’s sort indexes as the controller then handles keeping the tree consistent. Doing otherwise, I say hesitantly, would be programmer error.
I hope this hasn’t been to rambling, I’d love to hear of your opinions on this. Thanks again for reading the post and commenting, you’ve made me think again about my design decisions, hope you don’t mind that I’ve disagreed.
Thanks for your reply, Jonathan. I think that disagreement is a good basis for a discussion. Also, I’m glad you’re representing files in the tree of your app, because it’s a pretty good example of what I was trying to say.
With the filesystem, we have a case of model updating “programatically”. Files maybe added or removed behind our apps back. Our model might observe these changes via FSEvents and update itself. (The tree controller is notified via KVO.) Or we could have a separate model controller feeding the model, anyway it’s not a tree controllers job to do.
Model is already a tree by itself, and file items “know” about each other via parent/children relationships. Whether the file can be renamed/moved or not, is totally dependent of the file system - it’s not a decision of NSOutlineView delegate or NSTreeController to do. Also, moving a file might actually fail even after it looked like it would succeed. Again, only the model will know.
To make things harder, in addition to the content observed to be present in the actual file system, the model may contain other content which is only stored in the core data database. We might retain some data for the files that once deleted might reappear later, or like XCode, we could have “groups” or “smart groups” that only live in our model. So to sum up, we have many kinds of mirroring and syncing going on and we haven’t even introduced the UI yet.
NSTreeController is there to represent the model for the view layer, not to be the model.
Then things start to get more complicated. The possibility of having multiple views to the same model data requires introducing another model layer.
But what do you think about this so far?
I think I may know why I’m disagreeing with you (now only on some points
).
You say that the model should update itself and the tree controller shouldn’t be the one doing it. The way I was thinking is that the tree controller should take care of updating the model because it is a controller, now you bring forth the idea of a model controller that does the model updating and the tree controller can then be left to handle tree stuff.
This is how I’ve done it really in my app as I have an NSDocument subclass that is my model controller. So I have a (relatively) dumb model and a controller that does the decision-making, this I think is very much in-keeping with MVC.
You’re correct that the tree controller is there to represent the model for the view layer, hence why I’ve made it the one to update the sort indexes of the model after the drop acceptance (wherever that may finally be) as it knows all about the user’s requested ordering.
So do you think then that in the outline view’s delegate methods, the delegate should ask the model (or model controller) if the drop should occur?
In my app, I don’t quite represent the file system as you’re thinking of it. It’s like (the default setup of) Xcode, where all the files are reference arbitrarily and the “groups” aren’t represented on disk, hence why I’ve allowed arbitrary re-ordering of nodes and groups that doesn’t affect the file-system itself. I’m going to be using FSEvents in the future but at the moment I’ve implemented new file creation and drag and drop onto the tree from the Finder that then moves the files or simply creates a node that references the files’ current locations.
It’s this alternative view of the file-system that has caused me to allow the outline view’s delegate to allow/deny the drops. Interestingly though I have setup my model to control whether a node can be dragged in the first place, and other things like whether a particular node is a “group node” (see -outlineView:isGroupItem: ) or can collapse/expand. This resulted from trying to determine if the group was to be in the uppercase text like in iTunes. Originally checking the name of the group node was fine, but then I realised if a user called one of their own group “IMAGES” then it would return YES in -outlineView:isGroupItem: so the most reliable way is to give the group entity an isSpecialGroup attribute.
The file hosting is not working anyone mind re-uploading it on another server (Eg. Rapidshare) thanks
Yeah really sorry about that, an it seems that although I’ve paid $20.00 for 5GB of space on Wordpress I still can’t upload the zip file for the project. hmmm….. I’ll fix this. Bear with me.
here it is, I’ll update the post.
http://rapidshare.com/files/120385221/SortedTree.zip.html
Hi,
I’m using xCode 3.0 on PPC G4 Leopard 10.5.3.
When I open the SortedTree xcode project I get an error message, that the project was created with a newer version of xcode than mine. Thus I cannot compile and run the project. I’ve heard rumors that there is a xcode 3.1 in beta out there. Can anyone advice as to how to solve this issue.
Thank you very much.
Also Jonathan thank you very much for this tutorial. It is very valuable even just the source.
Greetings
Moritz
yeah sorry about that I’m running the iPhone SDK which has 3.1 so many improvemts it’s worth getting
Hi Jonathan,
Could you send me a working executable of SortedTree? I would really like to execute your programm because I don’t fully undestand the code yet. I think that would really help me to figure out what your code really does.
Thanks and greetings
Moritz
Hi I can’t get your code to compile because you built it with XCode 3.1 which is not yet available for us non-iPhone plebs.
Can you please release a version for 3.0?
hey jonathan, i can’t thank you enough for this. i’m a hack working on my first cocoa app and i’ve been banging my head against this for weeks it seems.
@Daniel,
I will when I get a sec! Before then you can create a new project with the same name and drag in all the files (including the NIB and info.plist) into Xcode after deleting the originals. That should do it.
@ian
You’re very welcome, people were so helpful when I started it’s good to be in the position to offer advice
“Before then you can create a new project with the same name and drag in all the files (including the NIB and info.plist) into Xcode after deleting the originals. That should do it.”
Tried that. No luck. It just hangs. I there’s an incompatibility with the version of IB you’re using and the official version 3.0. You used an unreleased beta so I expect there are some differences in the format of the XIB.
Thanks for the post. Very informative and thought provoking.
Hey Jonathan,
Would like to bug you again, not 100% related but since you mentioned NSPersistentDocument and core data in other posts…
I’ve got a core-data based app which is a non-document-based app (think iTunes, etc) yet I’d like to use the document-based machinery for it. In other words, I want to use NSPersistentDocument (ie. it’s undo features) + XSControllers architecture except that I’d like the document to be hard-wired to only file on disk — with no support for opening file types etc.
Is there some smart way to do this without changing Info.plist? Would you recommend this approach? Perhaps instantiating the Document myself and calling/simulating the makeWindowControllers machinery myself?
Do you want to have an app that has many windows that can show the user their data but in different ways? Like the way iTunes does when you double-click a playlist? Can you be more specific with what you’re trying to achieve in terms of using the program?
Jonathan,
Nice example app very helpful, was wondering if you where able to do the same thing but with undo working it, right now if you drag and drop a leaf from one place to another and hit undo the results from the undo are very random.
Rob
Hi Rob,
Sorry it’s taken me a while to get back to you on this, I’m in a rather internet-less state at the moment!
Unfortunately I haven’t been able to get undo working for it, and you’re right that its goes haywire! I think it would require some inventive creation of undo-groupings if it’s possible to fit it into the undo architecture of Core Data at all.
In Scribbler, I’ve actually turned of the Core Data undo management and written my own for that parts that make sense. The way I use this source list, for management of files and folders like iPhoto does, it’s not a requirement to make moving a node an undoable action, so I haven’t pursued it.
Sorry I can’t shed any more light on it, if you get somewhere with it, please let me know.
Jon
Nice article.
Small bug in NSTreeController_Extensions.m: the -flattenedContent method throws a valueForUndefinedKey exception, “the entity Group is not key value coding-compliant for the key descendants.”
The trouble is a mere copy/paste error, -flattenedNodes and -flattenedContent are very similar, and both do
[node valueForKey:@"descendants"].
However -flattenedContent is working with ESTreeNodes, not NSTreeNodes. The descendants key works in -flattenedNodes (calling your -descendants category on NSTreeNode), but clearly @”children” was intended here, to call ESTreeNode’s -children method.
That is, in -flattenedContent :
if (![[realNode valueForKey:[self leafKeyPath]] boolValue])
[mutableArray addObjectsFromArray:[realNode valueForKey:@"children"]]; // < -
This works fine.
-Bill
PS, dragging leaf objects and undoing seems to be working here - no problems.
Actually a better fix is to add a -descendants method to ESTreeNode, analogous to the category on NSTreeNode. Using children as described above is not quite right; won’t fully enumerate complex trees.
In ESTreeNode.m:
- (NSArray *)descendants;
{
NSMutableArray *array = [NSMutableArray array];
for (ESTreeNode *child in self.children) {
[array addObject:child];
if (![[child isLeaf] boolValue])
[array addObjectsFromArray:[child descendants]];
}
return [[array copy] autorelease];
}
Hi Bill,
Sorry for the late reply and for not updating that project properly. I should be moving over to my own server soon where I’ll be able to host whatever files I want to!
I realise now that I’d changed my implementation of -[NSTreeController flattenedContent] ages ago to be:
- (NSArray *)flattenedContent;
{
return [[self flattenedNodes] valueForKey:@”representedObject”];
}
which really helps in cutting down on such copy/paste errors. You’re correct that to be correct in your implementation that ESTreeNode needs a -descendants key. As I’d written the above method I hadn’t run into this one.
I need to have another look at undo, I’m surprised that its works for you. Maybe it was fixed be an OS update?…
Just tried it an it tells me that the undo manager is left in an invalid state with too many nested undo groups. Can you try and reproduce this on your machine?
Jon
Hi Jon,
Thanks for fix above. I believe I finally figured out a way to make undo work reliably. The basic problem is that while Core Data correctly does the undo and redo on the data, NSOutlineView doesn’t necessarily see the changes and as the display gets out of the sync with the model, eventually an undo/redo command is issued without the correct data on the stack to perform it, resulting in the “NSUndoManager is in invalid state, undo was called with too many nested undo groups” error.
In my my, the user must be able to sort the outline by various criteria, as well as sorting just a few selected rows within any given group. This has to be un-doable, and however the outline happens to be sorted/subsorted when it’s saved, it should re-open with the items in the same order. Obviously, that’s where I’m using your sortIndex idea. I set the sort descriptors as needed, update the sortIndices with your -updateSortOrderOfModelObjects method. When the document is re-opened, the sort descriptors are first set to one that sorts by the sortIndex, recreating the previous order. Undo works on everything, sorts, name edits, drag reordering, collapse/expand.
To get undo working in your sample project, I downloaded a fresh copy and made these changes. It may not be the best way, but it does seems to work.
1) In ESTreeController.m, change -reloadData so that it expands -and- collapses items according to the reloaded data. This is to prevent the problem of collapsing a group item, undo it and having work, then redoing and having the Edit menu flash correctly (indicating there was something on the stack that gets redone) but there being no visible change.
Something like:
- (void)reloadData;
{
[super reloadData];
NSUInteger row;
for (row = 0 ; row < [self numberOfRows] ; row++) {
NSTreeNode *item = [self itemAtRow:row];
if (![item isLeaf]) {
if ([[[item representedObject] valueForKey:@”isExpanded”] boolValue])
[self expandItem:item];
else
[self collapseItem:item];
}
}
}
=====
2) When Core Data undoes/re-does something, say a sort, the model data is changed correctly, and all the sortIndex values are restored, the outline view needs to be told to reload its data. NSUndoManagerDidUndoChangeNotification/NSUndoManagerDidRedoChangeNotification work for this. If you allow sorting on criteria other than sortIndex, as described above, Core Data may have changed a bunch of sortIndex values, and to get the un-done sort displayed, we need to tell the treeController to to re-sort by the sortIndex before anything happens to call -updateSortOrderOfModelObjects, which will overwrite all the un- or re-done sortIndexes with the item’s current, pre-undone position in the outline. If that makes sense…
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(undoManagerDidUndo:)
name:NSUndoManagerDidUndoChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(undoManagerDidRedo:)
name:NSUndoManagerDidRedoChangeNotification
object:nil];
}
- (void)managedObjectsDidChange:(NSNotification *)notification
{
[outlineView reloadData];
[treeController setSortDescriptors:nil];
[treeController setSortDescriptors:[self treeNodeSortDescriptors]]; // keep treeController sorted by sortIndex (except during a sort by name, etc)
}
- (void)undoManagerDidUndo:(NSNotification *)notification
{
[outlineView reloadData];
[treeController setSortDescriptors:nil];
[treeController setSortDescriptors:[self treeNodeSortDescriptors]]; // keep treeController sorted by sortIndex (except during a sort by name, etc)
}
====
3) I was constantly getting this error
SortedTree Error setting value for key path treeNodeSortDescriptors of object (from bound object [entity: TreeNode](null)): [ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key treeNodeSortDescriptors.
To stop it, I just turned off the NSTreeController sortDescriptors binding to ESAppDelegate’s treeNodeSortDescriptors, and instead, set the sort descriptors in code in -awakeFromNib or -windowControllerDidLoadNib.
====
4) These errors were constantly being logged:
-[ESTreeNode isSpecialGroup]: unrecognized selector sent to instance 0×193e00
-[ESLeafNode isSpecialGroup]: unrecognized selector sent to instance 0×1a4910
[ valueForUndefinedKey:]: the entity Leaf is not key value coding-compliant for the key isExpanded.
-[ESLeafNode canExpand]: unrecognized selector sent to instance 0×171f80
According to http://www.cocoabuilder.com/archive/message/cocoa/2008/6/9/209710
these seem to be due to a bug in Leopard which causes the -outlineView: isGroupItem: delegate method to sometimes be sent a dealloced object when Undo/Cmd-Z is used immediately after a creating a new object with CoreData when the NSOutlineView is set to SourceList style in InterfaceBuilder.
Whew… My fix was just to turn off the source list style. In my app I don’t use the style, so changed -outlineView: isGroupItem: to always return NO.
Well, sorry for the long post, but I wanted to report back since your code helped me get my CoreData/NSTreeController app off the ground. Shoot me an email at the address in this form if you’d like the edited project. Thanks again.