Using NSTreeController
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.
Michael 23:08 on June 3, 2008 Permalink |
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?
Jonathan Dann 23:31 on June 3, 2008 Permalink |
(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.
Michael 18:56 on June 4, 2008 Permalink |
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.
Jonathan Dann 19:07 on June 4, 2008 Permalink |
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.
Michael 19:15 on June 4, 2008 Permalink |
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.
Jonathan Dann 19:51 on June 4, 2008 Permalink |
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.
Michael 21:15 on June 4, 2008 Permalink |
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.
Jonathan Dann 21:29 on June 4, 2008 Permalink |
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.
Michael 21:52 on June 4, 2008 Permalink |
> 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.
Jonathan Dann 21:57 on June 4, 2008 Permalink |
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.
Jonathan Dann 00:20 on June 6, 2008 Permalink |
@”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.
Michael 18:50 on June 6, 2008 Permalink |
Ah, indeed, now everything makes sense. Thanks for your patience, that explains a lot.
Jonathan Dann 17:30 on June 7, 2008 Permalink |
Not at all, I wrote this to help people!
Jakob Dam Jensen 16:15 on July 5, 2008 Permalink |
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…
Ralph 19:35 on July 5, 2008 Permalink |
Hey Jonathan,
do you have a sample Application which combines your ESNode and XSController?
Jonathan Dann 20:27 on July 5, 2008 Permalink |
@ 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
Jonathan Dann 20:28 on July 5, 2008 Permalink |
@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
Joseph Crawford 04:22 on January 20, 2009 Permalink |
This is a very good explanation and has surely helped me understand this a bit better.
I am wondering if there is a benefit to using a tree controller rather than just handling the NSOutlineView groups/leafs using the objects.
What does binding bring to the table that not using them doesn’t?
Jonathan Dann 19:11 on January 20, 2009 Permalink |
Hi Joseph,
Glad it helped. As for the benefit, it’s mainly to help you ensure that changes to the model are automatically pushed to the tree controller without you having to intervene or tell the outline view to reload data, saving you the maintenance of the application logic in, what is likely, a fairly integral part of your app. Each time you add a node object to the managed object context (in the full-blown core data version I’ve also got on this blog) the tree controller will update its display.
It doesn’t come without its caveats, which were the main reason for writing this post. When I wrote it there were no decent tutorials for the Leopard world that included the new NSTreeNode, which I saw as the saving grace of this whole setup.
You save yourself writing the datasource methods, which is a both a blessing (not having to write them) and a curse (having to deal with the tree controller’s representation of things).
So really my answer, is “not much, until I’d written all this” but it was a challenge to get it working. What’s probably more important are the extensions on NSTreeNode themselves, which allow to to properly navigate the content of the tree.
Bindings with NSTreeController also allow you to remove a lot of other code, for example that which needs to know when the selected objects in the tree change. Without the tree controller you have to rely on notifications, which (in my experiments) are only sent for user-side updates. If you use bindings or simple KVO on the NSTreeController’s selectedIndexPath property then you save yourself some debugging hassle.
Have you tried using table view’s with and without NSArrayController? You see the same reduction in code when you do adopt bindings in those cases, too/
Sorry if this has been a bit waffling, I’m way too tired.
Jon
Ernest 16:32 on February 19, 2009 Permalink |
Jonathan, thank you so much for this example. I have successfully implemented it into my project, but now I have a question for you. Maybe I am just not doing something right, but here it is: I added an Outline View to my window, which comes with the standard NSScrollView -> NSOutlineView -> NSTableColumn -> NSTextFieldCell. This works perfectly with my Tree Controller. However, when I change the NSTextFieldCell to use the check box cell (NSButtonCell) and run the application I get nothing but “Check” for my list? Am I missing something with the node in which I have to change to support NSButtonCell vs NSTextFieldCell?
Jonathan 09:39 on February 24, 2009 Permalink |
Hi Ernest,
There could be an issue with incorrect bindings. What I did in my example was to bind the NSValueBinding (“Value” in IB) of the table column to the tree controller’s arrangedObjects.displayName keyPath. My first thought is that, as the NSValueBinding of the NSButtonCell refers to the on/off state of the button, the binding is turning them on if the bound string is not nil.
Not sure how you would solve this one as there are two values you need to consider here, the value and the label. This should be possible with bindings, but I’d do a long search on the cocoa-dev mailing list from Apple, this kind of thing comes up regularly.
Elise van Looij 14:44 on March 14, 2009 Permalink |
Ernest, you need to set the Value Transformer (in IB under the Value property) on your NSButtonCell, NSNegateBoolean, if I’m not mistaken and if the value of the cell is an integer.
Matthias 13:22 on June 19, 2009 Permalink |
Jonathan, thanks for the article. In the groupDescendants method, shouldn’t this line:
[groupsArray addObject:[node groupDescendants]];
rather read:
[groupsArray addObjectsFromArray:[node groupDescendants]];
Thanks, M.