A common UI concept in Mac development is that of the source list, which everybody knows from iTunes, iPhoto, etc. To do this, NSTreeController can come in particular handy, although it can come with a bit of a headache as the API is somewhat lacking and a proper model is essential to making this easy to use. I’m going to go over the solution that’s worked for me.
The starting point is the design of the model, we’ll begin by constructing a ‘node’ class that will represent a single item in our tree. A node will need a few properties to work with the tree controller well. The first is a ‘children’ property that’ll be an NSMutableArray intended to hold a series of our nodes. The second is a BOOL, ‘isLeaf’ that we’ll toggle depending on whether we want the node to have the possibility of children or not. Think of leaves as files and the opposite (I’ll call them groups) as folders, if you like.
Here’s how the code will look:
@interface ESNode : NSObject {
@protected
ESNode *_parent;
NSString *_nodeName;
NSMutableArray *_children;
BOOL _isLeaf;
}
@property(copy) NSString *nodeName;
@property(copy) NSMutableArray *children;
@property(assign) ESNode *parent;
@property(assign) BOOL isLeaf;
The problem is that objective-c 2.0 properties don’t have a mutableCopy keyword, so we’ll have to add the accessors ourselves, the isLeaf setter will do some magic too.
@synthesize nodeName = _nodeName;
@synthesize parent = _parent;
@dynamic isLeaf;
@dynamic children;
- (void)setIsLeaf:(BOOL)flag;
{
_isLeaf = flag;
if (_isLeaf)
self.children = [NSMutableArray arrayWithObject:self];
else
self.children = [NSMutableArray array];
}
- (BOOL)isLeaf;
{
return _isLeaf;
}
- (NSMutableArray *)children;
{
return _children;
}
- (void)setChildren:(NSMutableArray *)newChildren;
{
if (_children == newChildren)
return;
[_children release];
_children = [newChildren mutableCopy];
}
- (NSUInteger)countOfChildren;
{
if (self.isLeaf)
return 0;
return [self.children count];
}
- (void)insertObject:(id)object inChildrenAtIndex:(NSUInteger)index;
{
if (self.isLeaf)
return;
[self.children insertObject:object atIndex:index];
}
- (void)removeObjectFromChildrenAtIndex:(NSUInteger)index;
{
if (self.isLeaf)
return;
[self.children removeObjectAtIndex:index];
}
- (id)objectInChildrenAtIndex:(NSUInteger)index;
{
if (self.isLeaf)
return nil;
return [self.children objectAtIndex:index];
}
- (void)replaceObjectInChildrenAtIndex:(NSUInteger)index withObject:(id)object;
{
if (self.isLeaf)
return;
[self.children replaceObjectAtIndex:index withObject:object];
}
So we have a typical mutable array accessor and all the indexed accessors that allow you to place nodes anywhere in the children array. If you need to see more about why these are good to put in, then read the documentation on Key-Value Coding. We’ve also included a ‘parent’ property that is non-retained so all nodes can know about their parent object and a ‘nodeName’ which I commonly use to identify my nodes in the tree. The ‘isLeaf’ accessor will create either an empty mutable array ready for some children nodes or an array containing ’self’ if ’self’ has isLeaf set to YES. The only caveat here is is you adopt the NSCopying or NSCoding protocols then you have to set isLeaf before setting the children array.
In the implementation of our ESNode class we’ll include a few useful methods for searching and filtering the tree:
- (NSArray *)descendants;
{
NSMutableArray *descendantsArray = [NSMutableArray array];
for (ESNode *node in self.children) {
[descendantsArray addObject:node];
if (!node.isLeaf)
[descendantsArray addObjectsFromArray:[node descendants]];
}
return [[descendantsArray copy] autorelease]; // return immutable
}
- (NSArray *)leafDescendants;
{
NSMutableArray *leafsArray = [NSMutableArray array];
for (ESNode *node in self.children) {
if (node.isLeaf)
[leafsArray addObject:node];
else
[leafsArray addObjectsFromArray:[node leafDescendants]];
}
return [[leafsArray copy] autorelease]; // return immutable
}
- (NSArray *)groupDescendants;
{
NSMutableArray *groupsArray = [NSMutableArray array];
for (ESNode *node in self.children) {
if (!node.isLeaf) {
[groupsArray addObject:node];
[groupsArray addObject:[node groupDescendants]];
}
}
return [[groupsArray copy] autorelease]; // return immutable
}
These allow us to get any of our node objects and construct an array of their descendants down to the bottom of the tree. The final methods are our -init, -copyWithZone:, -initWithCoder: and -encodeWithCoder:
- (id)init;
{
if (![super init])
return nil;
self.nodeName = [NSString stringWithString:@"untitled"];
self.isLeaf = NO; // this will also set the children array to [NSArray array].
self.parent = nil;
return self;
}
- (id)initGroup;
{
return [self init];
}
- (id)initLeaf;
{
if (![self init])
return nil;
self.isLeaf = YES; // this will set the children array to an NSArray containing self.
return self;
}
- (id)copyWithZone:(NSZone *)zone;
{
ESNode *copy = [[[self class] allocWithZone:zone] init];
if (!copy)
return nil;
copy.nodeName = self.nodeName;
copy.isLeaf = self.isLeaf;
if (!self.isLeaf)
copy.children = self.children;
return copy;
}
- (NSArray *)keysForEncoding;
{
return [NSArray arrayWithObjects:@"isLeaf",@"children",@"parent",nil];
}
-(id)initWithCoder:(NSCoder *)coder;
{
if (![self init])
return nil;
for (NSString *key in self.keysForEncoding) {
[self setValue:[coder decodeObjectForKey:key] forKey:key];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder;
{
for (NSString *key in self.keysForEncoding)
[coder encodeObject:[self valueForKey:key] forKey:key];
}
- (void)setNilValueForKey:(NSString *)key;
{
if ([key isEqualToString:@"isLeaf"])
self.isLeaf = NO;
else
[super setNilValueForKey:key];
}
The nice thing about doing your NSCoding in this way is that concrete subclasses of ESNode can simply implement a single method to adopt NSCoding:
- (NSArray *)keysForEncoding;
{
return [[super keysForEncoding] arrayByAddingObjectsFromArray:[NSArray arrayWithObjects:@"key1",@"key2",@"key3",nil]];
}
Ok, so we have our node class and now we can use it with NSTreeController. In your AppController class (this may be anything, for example an NSDocument subclass) create an NSMutableArray instance variable and call it treeContent. This array will hold instances of our ESNodes (or a subclass of ESNode if you want to extend it).
In the next part I’m assuming you’re used to Interface Builder and your app controller is either the File’s Owner of your nib, or is instantiated in the nib. Create an instance of NSTreeController in Interface Builder and bind the @”contentArray” to the keypath to the ‘treeContent’ array. This tree controller will be powering an NSOutlineView. Click through on your NSOutlineView instance until you can edit the bindings for the NSTableColumn and bind the @”value” of the table column in the outline view to the tree controller’s @”arrangedObjects” controller key with the keypath @”nodeName”. You’ll also have to set the tree controller’s children keypath to @”children” and the leaf keypath to @”isLeaf”.
In the -init method of the class that contains your treeContent array, create a bunch of ESNodes and fill the tree, set some children and whatnot and all of this will appear in your NSOutlineView when you run the program.
So once your tree is created, whether by you in the -init method or by your user at runtime, you’ll most likely want to search through the tree from time to time. In your app controller you can generate a ‘flat’ version of the tree with the following method:
- (NSArray *)discreteProjectContent;
{
NSMutableArray *discreteProjectContent = [NSMutableArray array];
for (ESSourceNode *item in self.projectContent) {
[discreteProjectContent addObject:item];
if (!item.isLeaf)
[discreteProjectContent addObjectsFromArray:[item descendants]];
}
return [[discreteProjectContent copy] autorelease]; // return immutable
}
Try it out. You get all the nodes in the tree in one long array, you can then search through the array for the item you want, for example by nodeName, and get a pointer to any item in the tree.
For really great additions to NSTreeController I point you in the direction of Wil Shipley’s blog, which has a great few NSTreeController extensions. When used in conjunction with the code above, you can get a pointer to an item in the tree and call his -indexPathToObject: method and you can then select any object in the tree.



June 3, 2008 at 23:08 pm
Hey Jonathan,
great post, a quick question — in the XSControllers pattern you recently described with KATI, where do you put your NSTreeController? In the main nib file of the document window or in the sub-nib containing the actual controller client?
June 3, 2008 at 23:31 pm
(sorry about the code formatting here, will change my style sheet sometime)
That’s a good question actually, and is a little more involved than it sounds. I’ve changed it around quite a bit but have finally settled on a setup.
I’ve put it in a nib of an NSOutlineView controller, the outlinve view is the left side of an NSSplitView and so it’s in the nib of the child controller of the root view controller in the tree. This makes it a couple of levels away from my model controller - my NSPersistentDocument. The problem then comes when I want to insert a node in my tree from code in the persistent document, which is often the case as its where I do the dealings with the model.
To this end I’ve set the representedObject of all the view controllers to the NSPersistentDocument instance and then I can bind the tree controller to @”File’sOwner.representedObject.managedObjectContext”. To get at the tree controller from the document I’ve made a method which I added to XSWindowController:
- (XSViewController *)controllerForNibName:(NSString *)name;
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"nibName like %@",name];
return [[[self flatViewControllers] filteredArrayUsingPredicate:predicate] objectAtIndex:0];
}
and this calls
- (NSArray *)flatViewControllers;
{
NSMutableArray *flatViewControllers = [NSMutableArray array];
for (XSViewController *viewController in self.viewControllers) { // flatten the view controllers into an array
[flatViewControllers addObject:viewController];
[flatViewControllers addObjectsFromArray:[viewController descendants]];
}
return [[flatViewControllers copy] autorelease];
}
which is just the same depth-search in the -patchResponderChain method, just factored out.
So when I need the tree controller from the persistent document I can call
- (XSWindowController *)mainWindowController;
{
return [[[self windowControllers] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"windowNibName LIKE %@",ESDocumentWindowNibName]] firstObject];
}
- (ESOutlineView *)projectContentView;
{
return [[[self mainWindowController] controllerForNibName:ESHorizontalSplitViewNibName] valueForKey:@”projectContentView”];
}
- (NSTreeController *)treeContentController;
{
return [[[self projectContentView] infoForBinding:@”content”] valueForKey:NSObservedObjectKey];
}
(I show them all as I’ve just copied and pasted from XCode you can obviously condense these methods, but I use them all separately, the projectContentView is the outline view bound to the tree controller and ESDocumentWindowNibName is just an NSString *).
This works for me as I need to access the tree controller more often from the view controller (for outline view delegate methods) than in the persistent document. If I were to have it the other way round I’d programmatically create the NSTreeController in the persistent document and then bind to it (via representedObject) from the nib, but in the view controller I’d still need to get at is somehow.
With this setup I can’t seem to decide on the best setup that requires the least convoluted access to the controller from either side (the document or the view controller). I did think recently of putting it back in the document and then creating an NSTreeController ivar in the view controller which I then bind to the tree controller, but its *very* convenient having it in the nib.
I hope that I haven’t confused the matter! If you come up with a better encapsulated solution I’d like to hear it.
Thanks for reading, glad the code’s useful.
June 4, 2008 at 18:56 pm
Thanks for the explanation - I was expecting something in the lines of that but hoped that maybe you’ve got some other smart solution
I just recently started to look seriously into bindings, but after fighting with this and similiar design problems I came to think that bindings are awesome & great… for doing simple things. I’d love to be proven wrong but right now it seems to me that when trying to solve complex problems with bindings you end up with architecture which (indeed) contains much less code but is actually much more complex and time-consuming to maintain and develop.
That especially applies to using bindings with NS*Controller classes (as opposed to using simple bindings to custom controllers for field value/state tracking).
Anyways, just an opinion.
June 4, 2008 at 19:07 pm
You’re welcome, I’m still sticking with with bindings though. As my project has grown they’ve become more and more indispensable. There are some architectural considerations to overcome but the problems pale in comparison to doing it all with glue code.
The only issue I have with the setup I’ve described it the drilling through relationships to get to the controller, but there are worse things.
June 4, 2008 at 19:15 pm
BTW, in the “drilling” solution you described, does it solve the problem of accessing the NSTreeController from another view? I mean, not the OutlineView, not the Document itself but some other sub-view which needs to also fetch data from the tree controller or access it’s selection?
In particular, I mean a situation where you can use this (shared) NSTreeController from the Interface Builder.
June 4, 2008 at 19:51 pm
Ah I see what you mean, in my setup all the view controllers have the same representedObject (the document) so they can get it that way, or they use controllerForNibName and get the view controller that owns the nib the tree controller is in, the tree controller is then hooked up to an IBOutlet.
The outline view itself gets can get the tree controller too if it needs to like this:
- (NSTreeController *)boundTreeController;
{
return [[[self outlineTableColumn] infoForBinding:@”value”] valueForKey:NSObservedObjectKey];
}
So in IB a view can use the keypath @”File’sOwner.representedObject.treeController” as long as the nib is owned by a view controller. If the view is in the windowController’s nib its just as easy @”File’sOwner.document.treeController”. An additional window (like an inspector) still references the same document. I haven’t had a problem with it yet.
June 4, 2008 at 21:15 pm
Hmm, could be I’m missing something very obvious but, how do you actually bind to that controller in the IB? I mean, yeah, you can get the controller using some kind of “File’sOwner” path but, from the pov of the IB, it doesn’t see it as a TreeController… does it?
As far as I can see, you can use that keypath in a binding, but then you can’t select the “Controller Key” etc.
BTW, coming back to your original article, one note - on the created NSTreeController in the IB you need to set the “Children” to @”children”, otherwise you’ll get “Cocoa Bindings: Cannot perform operation if childrenKeyPath is nil.” error.
June 4, 2008 at 21:29 pm
Yeah sorry I forgot to put that in, also set the Leaf key path to @”isLeaf”. I’ll update the post.
I see your problem, can you not append arrangedObjects to the keyPath? The controller key is just a convenient shortcut anyway.
June 4, 2008 at 21:52 pm
> I see your problem, can you not append arrangedObjects to the keyPath? The controller key is just a convenient shortcut anyway.
Hmm… but if you use “File’sOwner.treeController.arrangedObjects” than you have no way of controlling what’s the object list and what’s the actual value. In other words, the real path is: “File’sOwner.treeController.arrangedObjects.nodeName” or rather: “”File’sOwner.treeController.arrangedObjects(each).nodeName”. But without the Controller Key you have no way of describing it - what’s the object list (array) and what’s the path to the individual object property to be used ie. in a given cell.
June 4, 2008 at 21:57 pm
I’m afraid I’m not following, sorry. If you’re on leopard I can iChat screen share and we can try and sort it out, either that or you can email me your XCode project?
If not then can you tell me what you’re trying to to and I might be able to help.
June 6, 2008 at 0:20 am
@”File’sOwner.treeController.arrangedObjects.nodeName” will return an NSArray of node names. The same happens when you have an NSArray and you call [myarray valueForKey:@"key"] the returned array contains the values for those keys.
June 6, 2008 at 18:50 pm
Ah, indeed, now everything makes sense. Thanks for your patience, that explains a lot.
June 7, 2008 at 17:30 pm
Not at all, I wrote this to help people!
July 5, 2008 at 16:15 pm
Thank you so much for this Jonathan - I’ve been trying to get something to work for the last two days, and somehow it all makes sense to me now - thanks to you.
Take care…
July 5, 2008 at 19:35 pm
Hey Jonathan,
do you have a sample Application which combines your ESNode and XSController?
July 5, 2008 at 20:27 pm
@ Ralph,
Sorry I’m afraid I don’t have anything prepared. The example app on katidev uses an NSOutlineView, so I’d just create an NSTreeController in that NIB and connect it up as I’ve described in this article.
Jon
July 5, 2008 at 20:28 pm
@Ralph
Sorry I’m afraid I don’t have anything prepared. The example app on katidev uses an NSOutlineView, so I’d just create an NSTreeController in that NIB and connect it up as I’ve described in this article.
Jon