Showing hierarchical data in the NSOutlineView

Today I want to talk about representing hierarchical data in a Mac application.

Cocoa offers the nice UI control NSOutlineView for this.
As the hierarchical data we can use a Data Abstract Schema instance which can contain various tables, fields, parameters, statements and many other objects.

Getting the DASchema

Retrieving the DASchema object from a Data Abstract server is quite simple:

NSString*serviceName =@"DASampleService"; NSString*urlString =@"http://MyServer:8099/bin"; NSURL*url =[NSURL URLWithString:urlString];   DARemoteDataAdapter *rda =[[DARemoteDataAdapter alloc] initWithTargetURL:url]; [[rda dataService] setServiceName:serviceName];   [rda beginGetSchemaWithBlock:^(DASchema *result){[self setSchema:result]; }];
## Defining our tree structure

The first thing we need to do is specify our tree-like hierarchy.
In order to simplify the sample, we will not implement the complete DASchema structure, only parts of it. The root object will be presented by the schema object with the tables and commands collection as children. Each table has a collection of fields as children.

So in addition to the existing DASchema elements, we need to declare some virtual elements in our tree. The schema, for example, has a collection of tables and commands. To avoid them being messed up, we want to have the collections separately, each in its own subfolder. The same is true for the fields collection inside each schema table.

These virtual classes should expose their name, note, icon and collection of children. All this info will be used by NSOutlineView for building the tree.

  //------------------------------------------------------//// OutlineNode.h//   #import   @interface DASchemaItem :NSObject-(NSString*)name; -(NSString*)note; -(BOOL)isLeaf; -(NSImage*)image; -(NSArray*)children; @end   @interface DASchemaCollection : DASchemaItem { DASchemaBaseObject *parent; }-(id)initWithParent:(DASchemaBaseObject *)aParent; @end   @interface DASchemaDataTableCollection : DASchemaCollection @end   @interface DASchemaDataTableFieldCollection : DASchemaCollection @end   @interface DASchemaCommandCollection : DASchemaCollection @end   //------------------------------------------------------//// OutlineNode.m//#import "OutlineNode.h"   @implementation DASchemaItem   -(BOOL)isLeaf {returnNO; }   -(NSImage*)image {returnnil; }   -(NSArray*)children {returnnil; }   -(NSString*)name {return@""; }   -(NSString*)note {return@""; }   @end   @implementation DASchemaCollection   -(id)initWithParent:(DASchemaBaseObject *)aParent { self =[super init]; if(self !=nil){ parent =(DASchemaBaseObject *)aParent; }return self; }   -(void) dealloc {[super dealloc]; }   -(NSImage*)image {return[NSImage imageNamed:@"Collection.png"]; }   @end   @implementation DASchemaDataTableCollection   -(NSArray*)children {return[(DASchema *)parent tables]; }   -(NSString*)name {return[NSString stringWithFormat:@"TABLES (%ld)", [[(DASchema *)parent tables] count]]; }   @end   @implementation DASchemaDataTableFieldCollection   -(NSArray*)children {return[(DASchemaDataTable *)parent fields]; }   -(NSString*)name {return[NSString stringWithFormat:@"Fields (%ld)", [[(DASchemaDataTable *)parent fields] count]]; }   @end   @implementation DASchemaCommandCollection   -(NSArray*)children {return[(DASchema *)parent commands]; }   -(NSString*)name {return[NSString stringWithFormat:@"COMMANDS (%ld)", [[(DASchema *)parent commands] count]]; }
We then need to extend some DASchema classes with the same generic methods as above (which return information about children and icons). Fortunately, Objective-C allows to do that quite easily with help of Categories.
  // Append it to OutlineNode.h   @interface DASchemaBaseObject (OutlineNode)-(NSArray*)children; -(BOOL)isLeaf; @end   //------------------------------------------------------// Append it to OutlineNode.m   @implementation DASchemaBaseObject (OutlineItem)   -(NSImage*)image {returnnil; }   -(NSArray*)children {returnnil; }   -(BOOL)isLeaf {return[[self children] count] < 1; }   @end   @implementation DASchema (OutlineItem)   -(NSArray*)children { DASchemaCollection *tables =[[[DASchemaDataTableCollection alloc] initWithParent:self] autorelease]; DASchemaCollection *commands =[[[DASchemaCommandCollection alloc] initWithParent:self] autorelease]; NSArray*result =[NSArray arrayWithObjects:tables, commands, nil]; return result; }   @end   @implementation DASchemaDataTable (OutlineItem)   -(NSArray*)children { DASchemaCollection *fields =[[[DASchemaDataTableFieldCollection alloc] initWithParent:self] autorelease]; NSArray*result =[NSArray arrayWithObjects:fields, nil]; return result; }   -(NSImage*)image {return[NSImage imageNamed:@"Table.png"]; }   @end   @implementation DASchemaDataTableField (OutlineItem)   -(NSArray*)children {returnnil; }   -(NSImage*)image {if(self.isPrimaryKey)return[NSImage imageNamed:@"KeyField.png"]; return[NSImage imageNamed:@"RegularField.png"]; }   @end   @implementation DASchemaCommand (OutlineItem)   -(NSArray*)children {returnnil; }   -(NSImage*)image {return[NSImage imageNamed:@"Command.png"]; }   @end
Now we are ready to bind our schema object to NSOutlineView. I prefer to do that using the *Cocoa Bindings* approach. It requires almost no coding and the whole job can be done in the *Interface builder* that loads in the working area when we select the xib file.

Binding NSOutlineView

As the data source for the NSOutlineView, we can use the NSTreeController class. You can locate it in the Object Library, then drag and drop it to the document designer.

After that, select the recently added tree controller object and go to the *Utilities *view *Attribute Inspector *. In the *Tree Controller *section, we need to set the key paths for obtaining the children collection and for defining if the tree node is a leaf or can be expanded.

Also in the *Object Controller *section, we need to set the *Class *as the *Mode *and the *DASchemaBaseObject *as the *Class Name *, then add name & note as Keys. These keys will be available for selecting later when we will bind the outline view columns to this tree controller.

Go to the Bindings Inspector and bind the tree controller to the children method of the schema property.

Now the tree controller is configured and we can bind the outline view columns to the key paths exposed by the tree controller.
Bind the SchemaItem column.

Then bind the Note column.

And that’s all. When we run the application, we will get something like this:

Note that Cocoa Bindings provides two-way binding, so we can edit tree node items just in outline view without any additional efforts.

Adding Images to the outline view items

You may notice on the screens above that NSOutlineView shows its content with images. To do that, we’ve reused the ImageAndTextCell class taken from the SourceView Sample.

We have included it to our sample project and selected it as the custom class of the outline view column cell.

We then need to declare the support of the NSOutlineViewDelegate protocol for our window controller and implement the
outlineView:willDisplayCell:forTableColumn:item: method which will be called by the outline view control before drawing its cells.

-(void)outlineView:(NSOutlineView*)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)tableColumn item:(id)item {// draw images for first column onlyif([@"Schema Item" compare:[[tableColumn headerCell] title]]== NSOrderedSame){   NSImage*image =nil; if([item respondsToSelector:@selector(image)]) image =[item image]; elseif([item isKindOfClass:[NSTreeNode class]]) image =[[item representedObject] image];   ImageAndTextCell *c =(ImageAndTextCell *)cell; [c setImage:image]; }}
The source code for the sample described in this article can be downloaded from here: [OutlineViewSample.zip](http://rorox.remobjects.com/alexk/OutlineViewSample.zip "OutlineViewSample.zip")

Thanks for your attention.