NSOutlineView Example

in Cocoa Programming

xcodeiconNSOutlineView can be a real bitch to work with. If you’re getting stuck, here’s a simple example that uses a tree of objects using an NSDictionary for the root object.

It needs some extra features to be truly useful, but you get the idea.

The main trick to this class is that we use the information within the NSDictionary to respond to each of the delegate methods that we’re required to implement for NSOutlineView to work.

If you don’t want to use an NSDictionary, then modifying this class or writing one from scratch actually isn’t that hard, but you do need to pay attention to each of the delegate methods and work out exactly what they’re doing.

OutlineDictionary.h:

#import <Cocoa/Cocoa.h>

@interface OutlineDictionary : NSObject {
    NSMutableDictionary         *root;
}

@property (assign,readonly) NSMutableDictionary *root;
@end

OutlineDictionary.m:

#import "OutlineDictionary.h"

@implementation OutlineDictionary
@synthesize root;

- (id)init
{
    if ((self = [super init]))
    {
        root = [[NSMutableDictionary new] retain];
    }
    return self;
}

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

- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
    // The NSOutlineView calls this when it needs to know how many children
    // a particular item has. Because we are using a standard tree of NSDictionary,
    // NSArray, NSString etc objects, we can just return the count.
   
    // The root node is special; if the NSOutline view asks for the children of nil,
    // we give it the count of the root dictionary.
   
    if (item == nil)
    {
        return [root count];
    }

    // If it's an NSArray or NSDictionary, return the count.
   
    if ([item isKindOfClass:[NSDictionary class]] || [item isKindOfClass:[NSArray class]])
    {
        return [item count];
    }

    // It can't have children if it's not an NSDictionary or NSArray.

    return 0;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
    // NSOutlineView calls this when it needs to know if an item can be expanded.
    // In our case, if an item is an NSArray or NSDictionary AND their count is > 0
    // then they are expandable.
   
    if ([item isKindOfClass:[NSArray class]] || [item isKindOfClass:[NSDictionary class]])
    {
        if ([item count] > 0)
            return YES;
    }
   
    // Return NO in all other cases.
   
    return NO;
}

- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
{
    // NSOutlineView will iterate over every child of every item, recursively asking
    // for the entry at each index. We return the item at a given array index,
    // or at the given dictionary key index.
   
    if (item == nil)
    {
        item = root;
    }
   
    if ([item isKindOfClass:[NSArray class]])
    {
        return [item objectAtIndex:index];
    }
    else if ([item isKindOfClass:[NSDictionary class]])
    {
        return [item objectForKey:[[item allKeys] objectAtIndex:index]];
    }
   
    return nil;
}

- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
    // NSOutlineView calls this for each column in your NSOutlineView, for each item.
    // You need to work out what you want displayed in each column; in our case we
    // create in Interface Builder two columns, one called "Key" and the other "Value".
    //
    // If the NSOutlineView is after the key for an item, we use either the NSDictionary
    // key for that item, or we count from 0 for NSArrays.
    //
    // Note that you can find the parent of a given item using [outlineView parentForItem:item];
   
    if ([[[tableColumn headerCell] stringValue] compare:@"Key"] == NSOrderedSame)
    {
        // Return the key for this item. First, get the parent array or dictionary.
        // If the parent is nil, then that must be root, so we'll get the root
        // dictionary.
       
        id parentObject = [outlineView parentForItem:item] ? [outlineView parentForItem:item] : root;
       
        if ([parentObject isKindOfClass:[NSDictionary class]])
        {
            // Dictionaries have keys, so we can return the key name. We'll assume
            // here that keys/objects have a one to one relationship.
           
            return [[parentObject allKeysForObject:item] objectAtIndex:0];
        }
        else if ([parentObject isKindOfClass:[NSArray class]])
        {
            // Arrays don't have keys (usually), so we have to use a name
            // based on the index of the object.
           
            return [NSString stringWithFormat:@"Item %d", [parentObject indexOfObject:item]];
        }
    }
    else
    {
        // Return the value for the key. If this is a string, just return that.
       
        if ([item isKindOfClass:[NSString class]])
        {
            return item;
        }
        else if ([item isKindOfClass:[NSDictionary class]])
        {
            return [NSString stringWithFormat:@"%d items", [item count]];
        }
        else if ([item isKindOfClass:[NSArray class]])
        {
            return [NSString stringWithFormat:@"%d items", [item count]];
        }
    }
   
    return nil;
}

@end
9 Comments

9 Comments

  1. Great Googly-moogly!

    Fantastic help. Thank you.

  2. Malcolm Hall

    Icons?

  3. twocranes

    Nice, for XCode 3+, but under Tiger and XCode 2.5?

    A quick look shows only @synthesize and @property, which should be easy enough to back-date. Any behavioural issues?

  4. twocranes

    yes, you are depending on NSOutlineView -parentForItem: which postdates XCode 2.5

    Another observation, you support child-bearing nodes of NSArray and NSDictionary, but not NSSet. Is this solely because sets are considered unordered, and cannot be sorted?

    • To be honest, I don’t think it is unreasonable to expect Tiger people to update to Leopard. It’s 2009! All the cool frameworks are in Leopard. Besides, Snow Leopard is right around the corner.

      I didn’t bother with NSSet because I don’t use it personally. One could add support … but I don’t think it would be very deterministic with the ordering issue you mention. An exercise for the reader? :)

  5. The init method leaks:

        root = [[NSMutableDictionary new] retain];

    Calling ‘new’ is equivalent to alloc/init, therefore the additional retain causes a retainCount of 2. You decrement the retain count only once in dealloc, so it’ll leak.

  6. Thanks, this was very handy! I’ve implemented most of a property list editor in my level editing tool that I’m working on! The only downside to your example is that NSOutlineView apparently requires all items to be unique, so when I’ve got a plist that contains a dictionary describing a point at 0, 0 I end up with duplicate items – instead of seeing x:0, y:0, I see x:0, x:0. Same is also true for my empty keys that have values set to “”.

    Do you have any thoughts on that? I’m going to keep poking at it, because aside from that edge case, this is a great starting point!

Leave a Reply

Using Gravatars in the comments - get your own and be recognized!

XHTML: These are some of the tags you can use: <a href=""> <b> <blockquote> <code> <em> <i> <strike> <strong>