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

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




May 26, 2008 at 11:40 am
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.
May 26, 2008 at 12:37 pm
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.
May 27, 2008 at 11:49 am
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?
May 28, 2008 at 18:12 pm
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.
June 5, 2008 at 19:51 pm
The file hosting is not working anyone mind re-uploading it on another server (Eg. Rapidshare) thanks
June 6, 2008 at 0:08 am
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.
June 6, 2008 at 0:10 am
here it is, I’ll update the post.
http://rapidshare.com/files/120385221/SortedTree.zip.html
June 7, 2008 at 12:51 pm
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
June 7, 2008 at 13:10 pm
yeah sorry about that I’m running the iPhone SDK which has 3.1 so many improvemts it’s worth getting
June 8, 2008 at 16:16 pm
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
June 17, 2008 at 21:57 pm
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?
June 17, 2008 at 22:47 pm
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.
June 18, 2008 at 10:26 am
@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
June 18, 2008 at 21:23 pm
“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.
June 21, 2008 at 23:27 pm
Thanks for the post. Very informative and thought provoking.
June 26, 2008 at 16:31 pm
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?
June 26, 2008 at 23:52 pm
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?