NSOutlineView — Inside Out 2

A slightly more complex data source and expandable and collapsibe features

Some good months ago I started a series on NSOutlineView that I didn’t imagine it will stir such an interest. To my astonishment, googling NSOutlineView ranks my post fourth in the search, immediately after Apple’s official documentation and some other two links:

NSOutlineView article on alauda.ro post — fourth in Google search rank !

NSOutlineView article on alauda.ro post — fourth in Google search rank !

This intriguing situation (nevertheless inspiring for me) might have to do with the fact that this class is one of the most difficult to understand and use, from the entire Cocoa framework, given its finicky delegates, complex inheritance model and complicated customization options.

Today I will take it one step further adding a bit more complexity to the previous example. I believe this will help wrapping up the knowledge gained with previous article, and will make a good exercise. Please try and code, do not copy-paste. It will be better for your learning curve because it will help memorizing some of key concepts. I have omitted the needed steps of adding a NSOutlineView control in XCode interface builder section, declaring it’s controller (or owner class) as delegate and data source and connecting the IBOutlet to the NSOutlineView control. Don’t forget to do these, otherwise nothing will work !

NSOutlineView — a very simple example

As a reminder, in the previous post I used a very simple array as data source for the NSOutlineView. The result was a simple display of the array content, one entry each row. Like that:

The simplest OutlineView appThe application had only its delegate class, AppDelegate.m which acts both as NSOutlineView delegate and data source. For review, I have included below the complete implementation code from previous example. Before continuing, go over the code and try to understand each line; it will help reading the comments; note also the pragma directive:

#pragma mark -
#pragma mark OutlineView Data Source Delegate Methods

that improves the code structuring and creates more order; I always use these to ease code navigation, improve my code structure in an orderly manner and have the option to quickly access the relevant sections in XCode. Succesful coding and good software needs order. When I will write about documenting code (for example documenting with DOxigen or navigating inside a huge and complex class) you will understand why is so important. Don’t forget to assign the delegate and the data source of the NSOutlineView control, in XCode’s interface builder and connect the control outlet. We will make one more assumption — for the sake of increasing complexity — and create two columns within the NSOutlineView control, specifying the identifiers for each column, fColumn and sColumn.

Column identifiers in XCode interface builder.

Column identifiers in XCode interface builder.

Please note that each column object is an instance of NSTableColumn class. This is important when we will discuss the inheritance model of NSOutlineView. Without proper understanding of the inheritance model, you cannot customize your control. The implementation of NSOutlineView looks like that:

//
//  AppDelegate.m
//  NSOutlineView-Simplest
//
//  Created by AlaudaProjects on 30.04.2012.
//

#import "AppDelegate.h"
@implementation AppDelegate
@synthesize window = _window;

- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    //initialize stuff -> nothing for the moment; maybe we will think about something, later
}

- (void)awakeFromNib
{
    // this is the simplest data source you might get
    dataSource = [NSArray arrayWithObjects:@"John", @"Mary", @"George", nil]; // are we leaking ?
// if I am debugging, show the entire data structure in the console:
#if DEBUG
    NSLog(@"%@", dataSource);
#endif
// if I am debugging, inform the that the nib is awoke. very basic, I will extend it later.
#if DEBUG
    NSLog(@"nib awaken");
#endif
}

- (void)dealloc
{
    [dataSource release]; // always release your allocated memory; is this really needed in this example or not ?
    [super dealloc];
}
// organize your code
#pragma mark -
#pragma mark OutlineView Data Source Delegate Methods

// -------------------------------------------------------------------------------
//  Returns the object that will be displayed in the tree
//  – outlineView(OBJECT):child(NSInteger):ofItem:(id)
// -------------------------------------------------------------------------------
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
{
    // indicate which is the child of a particular item
    // the root is always associated with "item = nil" !
    // since we have a very simple array, with no complex parent-children relationship
    // we return each object found at each index value of the array:
    return [dataSource objectAtIndex:index];
}// end – outlineView(OBJECT):child(NSInteger):ofItem:(id)

// -------------------------------------------------------------------------------
//	– outlineView(OBJECT):child(NSInteger):ofItem:(id)
// -------------------------------------------------------------------------------
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
    // this data source method asks whether a particular item in the tree is expandable
    // basically, this give the control it's characteristic trait
    // again, the root node is identified by a nil item, then this item is expandable
    // for all other, i.e. children, them item is not expandable (item != nil)
    if (item == nil) {
        return YES;
    }
    return NO;
}// end

// -------------------------------------------------------------------------------
//	– outlineView(OBJECT):child(NSInteger):ofItem:(id)
// -------------------------------------------------------------------------------
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
    // the third datasource delegate method asks for the number of children of a particular item
    // in order to display the items you need to know how many you have; root nodes have children.
    // in our case, the items are also "roots", thus we have to pass the number of entries
    // in the dataSource array:
    return [dataSource count];
}// end

// -------------------------------------------------------------------------------
//	– outlineView(OBJECT):child(NSInteger):ofItem:(id)
// -------------------------------------------------------------------------------
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
    // the last datasource delegate method asks where to display each item.
    // basically this populates the control with actual data.
    // assuming we have two columns, we pass the data to populate rows in first column
    // and a nil object, to have the second column empty.
    // note the two different ways in which we can perform the check (one is commented out).
    // you will find the compact one in many examples.
    if ([[tableColumn identifier] isEqualToString:@"fColumn"]) {
        /*
         if (item == nil) {
         return @"My family";
         }
         */
        return item == nil ? @"My family" : item; // returns the result of expression evaluation
    }

    if ([[tableColumn identifier] isEqualToString:@"sColumn"]) {
        //return item; if you want or empty, via a nil object:
        return nil;
    }

    // safe harbor: in case something wrong happens, the method must return something.
    return item;

}// end

@end

More complex data source, collapsible items and header data

Let’s elaborate this model and add a bit more complexity. We want to get something like this:

A more elaborated example: two column display, aggregate info in column header and collapsible items.

A more elaborated example: two column display, aggregate info in column header and collapsible items.

For this, I have created a new project and an additional class, OutlineViewController.m, and made it conformant to both protocols:

//
//  OutlineViewController.h
//  NSOutlineView_Simple_Two_Dictionaries
//
//  Created by AlaudaProjects on 16.04.2012.
//

#import <Cocoa/Cocoa.h>

@interface OutlineViewController : NSOutlineView <NSOutlineViewDataSource, NSOutlineViewDelegate> {
    NSDictionary *firstParent;
    NSDictionary *secondParent;
    NSArray *list;

    IBOutlet NSOutlineView *myOutlineView;
}

@end

Let’s initialize the data source in OutlineViewController.m:

#import "OutlineViewController.h"

@implementation OutlineViewController

NSException *myException;

- (id)init
{
    if (self = [super init]) {
        // something nice for your family
        firstParent = [[NSDictionary alloc] initWithObjectsAndKeys:@"James",@"parent",[NSArray arrayWithObjects:@"Mary",@"Charlie", nil],@"children", nil];

        secondParent = [[NSDictionary alloc] initWithObjectsAndKeys:@"Elisabeth",@"parent",[NSArray arrayWithObjects:@"Jimmie",@"Kate", nil],@"children", nil];

        list = [NSArray arrayWithObjects:firstParent,secondParent, nil];

        myException = [NSException exceptionWithName:@"Test exception" reason:@"Something bad happened" userInfo:nil];

    }
    return self;
}

Note both dictionaries allocated in init() method; reason for this later. Also, don’t forget to release them both:

- (void)dealloc
{
    [firstParent release];
    [secondParent release];
    [super dealloc];
}

Now, getting back to delegate methods. Remember, we have four datasource delegate methods that we have to override in order to have the control working properly. These are:

  • outlineView:isItemExpandable: → specify which item is expandable;
  • outlineView:numberOfChildrenOfItem: → specify the number of item children;
  • outlineView:child:ofItem: → define the hierarchy (who is whose child)
  • outlineView:objectValueForTableColumn:byItem: → specify what and where to show the values;

These are the four things you have always to do in order to have a functional NSOutlineView. These are all mandatory to be overriden with ONE important exception: whenever you wish to use Cocoa Dynamic Bindings with NSOutlineView. With bindings you don’t have to override these methods, but we will discuss this in a dedicated article. However, for the sake of properly understanding how NSOutlineView works, we will use the non–bindings approach for now.

First we decide that root items are the dictionaries and arrays; the rest are children. Thus, root items are expandable:

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
    if ([item isKindOfClass:[NSDictionary class]] || [item isKindOfClass:[NSArray class]]) {
        return YES;
    }else {
        return NO;
    }
}

Then, we return the number of children for each root node; note that in any other situation the method returns 0 — no children:

- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{

    if (item == nil) { //item is nil when the outline view wants to inquire for root level items
        return [list count];
    }

    if ([item isKindOfClass:[NSDictionary class]]) {
        return [[item objectForKey:@"children"] count];
    }

    return 0;
}

Pause and examine a bit the logic of the above code. First, we have two root entries that correspond to both dictionaries, firstDictionary and secondDictionary. Thus, we have to get two entries in the NSOutlineView, that correspond to each dictionary; accordingly we count the number of entries in the list array (see the init() method).

Then we have to return the number of entries (childern) in each dictionary, thus we count the number of items that correspond to the @children key in each dictionary. This will return, for each root (= dictionary), two entries.

Then, to be compliant to the best practices, we have to return a default value, in case something goes wrong. Home assignment: What can go wrong ?

After we set the number of children and root nodes, we have to inform the delegate about the hierarchy: who is whose children:

- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
{

    if (item == nil) { //item is nil when the outline view wants to inquire for root level items
        return [list objectAtIndex:index];
    }

    if ([item isKindOfClass:[NSDictionary class]]) {
        return [[item objectForKey:@"children"] objectAtIndex:index];
    }

    return nil;
}

Again, examine the logic. When the item is nil (= root node), we have to pass the corresponding value from the list array that holds the dictionaries, i.e. we pass each dictionary, for each root node:

    if (item == nil) { //item is nil when the outline view wants to inquire for root level items
        return [list objectAtIndex:index]; // returns dictionary objects
    }

If we do not have a root node (= nil item), then we have to pass the actual values, i.e. the corresponding values for each index, in each array identified by the @children key, from each dictionary (re-examine the dictionaries in the init() method). Pause a second and make sure you understand this:

    if ([item isKindOfClass:[NSDictionary class]]) { // if we have a dictionary...
        return [[item objectForKey:@"children"] objectAtIndex:index]; // the value within each array from the dictionary
    }

Again, return nil in case something very wrong happens. And same assignment as above: what can come wrong ?

Lastly, we have to inform which value to be displayed in each row and in which column of the NSOutlineView. To identify the columns we use the column identifiers:

- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)theColumn byItem:(id)item
{

    if ([[theColumn identifier] isEqualToString:@"children"]) { // in the column "children"...
        if ([item isKindOfClass:[NSDictionary class]]) { // if we have a dictionary...
            // ... then write something informative in the header (number of kids + "kids")...
            return [NSString stringWithFormat:@"%li kids",[[item objectForKey:@"children"] count]];
        }
        return item; // ...and, if we actually have a value, return the value
    } else {
        if ([item isKindOfClass:[NSDictionary class]]) { // in the "parent" column
            return [item objectForKey:@"parent"]; // just write the value for the @parent keys
        }
    }

    return nil; // if shit happens, don't blame it on me !
}

First we check the NSOutlineView column identifier and, if equal to children we check if we have a dictionary object. If not, we pass the item; if we have a dictionary object, we want to display a custom text that displays the total of children and the word “kids”:

        if ([item isKindOfClass:[NSDictionary class]]) {
            return [NSString stringWithFormat:@"%li kids",[[item objectForKey:@"children"] count]];

        }

If the NSOutlineView column identifier is not children, then it cannot be anything else than parent, we check if there is a dictionary object and return the value for key @parent, i.e. the name of the parent (examine the dictionaries in the init() method):

        if ([item isKindOfClass:[NSDictionary class]]) {
            return [item objectForKey:@"parent"];
        }

That’s it. Again, write the code with your own fingers, don’t copy–paste ! Don’t forget to assign the delegates, connect the outlet and identify the columns of NSOutlineView control. Then run the program. You should get a two-column NSOutlineView control with expandable and collapsible parent entries, displaying aggregate info in children column header, with each name of each children for the corresponding parent:

A more elaborated example: two column display, aggregate info in column header and collapsible items.

A more elaborated example: two column display, aggregate info in column header and collapsible items.

Wrap it up for today

At this stage you should be quite familiar with the rule of thumb when comes to NSOutlineView: start with outlets, assign delegate and override these four methods. Note again, these should not be overriden if Cocoa Dynamic Bindings are used ! These are the 3 mandatory steps in order to get a functional NSOutlineView without bindings. These are the basics. In the following articles we might explore examples using datasources that are closer to real–life situations (complex dictionaries, plist files, core data etc), but the principles remain the same. If you understood the above, the rest is piece of cake.

In following articles we will concentrate on the complex inheritance model of NSOutlineView class in order to better understand the rest of delegate methods; we will enhance today’s application with some other overriden delegate methods to understand what these are for and how can you use them; we will customize the look and feel using methods inherited from inherited classes.

Enjoy, and, if you like it, please comment and thumb it up on Facebook.

Update: May 24th, 2013.

I have created a code repository on Bitbucket to share code discussed in this blog. You can find it here: http://bitbucket.org/AlaudaProjects.

10 Comments to NSOutlineView — Inside Out 2

  1. Szabolcs says:

    An excellent tutorial again! Thank you!

    • Thanks for commenting. It would be interesting to know what people would like to read and what tutorials to write. Let me know.
      Regards,
      AP

      • Szabolcs says:

        E.g. I am interested in this topic deeper too. How can I use NSOutlineView as a filter. On the left hand side there are parents’ names while right hand side all children’s names. If I select a parent the right hand side reloads and the view shows only his/her children.

  2. awado says:

    Thanks for both tutorials. Started with Obj-C a week ago and it feels like Mt. Everest. Your code has helped me a lot. Your Google ranking is well deserved. Now I’m trying to translate it into a source list view, which is new in XCode.

    • Thank you, Awado, for your comments and feedback. If you are new to Obj-C, I would recommend at least two books for you: Cocoa Design Patterns (Buck and Yacktmann) and Kochan’s Programming in Objective-C (5th edition already). Both are excellent value for the money and will ease the path for you. On the new view-based NSOV, one of my next articles will cover this topic. Stay tuned. 🙂
      Regards,
      AP

  3. Project Mayhem says:

    Hi AlaudaProjects,

    thanks a lot for these marvelous OutlineView-Tutorials. Like Szabolcs i’m also interested in using an OutlineView as a filter: e.g. in the first column i can select(=highlighted with blue color) a non-expandable film-category(action, drama, scifi, …) and then in the second and third columns the family names and first names of the famoust actors of the selected film-category(one actor per row) will be shown.

    I hope you will find some time for “NSOutlineView — Inside Out 3” soon… 😉

    Greetings,
    Hans

    • Hi, Hans. Thank you for your comments and appreciation. As told previously, I have a very demanding job that keeps me from constantly updating this blog. However, the OutlineView series will continue. Moreover, I have created a repository in Git where code can be downloaded. Will post soon, number three is in draft for two months (for now, I think 😉
      Regards,
      AP

  4. Rakesh says:

    Great article. It helped me a lot! Thanks. 🙂

    This maybe nitpicking but you are using ‘The implementation of NSOutlineView looks like’ when you are actually showing the .m file of AppDelegate. Another thing the controller need only be an NSObject subclass right? The subclassing of NSOutlineView -‘@interface OutlineViewController : NSOutlineView’ doesn’t make sense.

    • Hi, Rakesh. Thank you for your comments. I am afraid I don’t understand very well your questions. I was looking over the code again but, honestly, I might miss understanding the “app delegate” part in your comment. On NSObject… why should not make sense overriding NSOutlineView and only NSObject ? The controller can be an instance of any class, as long as it pertains and is able to behave as a controller.
      Regards, and thanks again.
      AP

      Edit; June 15, 2013
      I was a bit confused by the comment, but today I looked more carefully over the article and I think I got your point.
      Well, I used the app delegate just because of convenience: it was there and was easier to use it. Of course, you are not constrained to declare conformance to NSOutlineView delegate and data source protocols to a specific class type. I might have very well used another class, an instance of NSObject, declare that class conformant to both protocols, override NSOutlineView delegate methods there and then link it accordingly in Interface Builder.
      Secondly, about instance of NSOutlineView:
      Since Objective-C is a very dynamic programming language, it does not care too much about what class type one uses for a controller as long as this generic controller is a custom object descending from NSObject. Of course, I could have used:

      @interface OutlineViewController : NSObject
      

      Implementing an instance that descends directly from NSOutlineView might have (arguably) the advantage of easier (aka, “simplified”) introspection. Mind you, this is a very simple example. If I’d manage to keep my time on track, you will see in my future articles that there are several other possibilities. It is up to you which one you will choose. But again, when programming in Obj-C, try to free your mind from the constraints of other OOP languages. You have a very dynamic world here, with its specific advantages and own perils.

      Hope this explains a bit.
      Regards,

      AP

Leave a Reply

Your email address will not be published. Required fields are marked *