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.