I have a love and hate relationship with Apple developer documentation. Official documentation about NSOutlineView class and delegates are very good examples. Sometimes it is a real pleasure to follow their published information and design code and applications. But, from time to time, starting up a very basic, simple example and, further on, getting your more complex project on track is a nightmare. I agree, maybe documentation should not be devised with the beginner in mind. In the end, there are tons of books and examples [maybe] on the net to help a novice grasp some elementary concepts. But I did a thorough search on net and first pages with links provided by Google have, mostly, very complex examples that might be a bit difficult to understand. Moreover, some of the methods required are very poorly explained and these are critical to make some controls work and to actual do things you’d expect from it. This is the first part of a series I intend to maintain on NSOutlineView class, implementation, examples and other how to’s. Most of the information included is based on my previous experience using this class and some advices that I dare to consider for the newbie that wants to get fast to some results. Second in series can be accessed here.
What is an NSOutlineView?
Quoting from Apple:
NSOutlineView
is a subclass ofNSTableView
that uses a row-and-column format to display hierarchical data that can be expanded and collapsed, such as directories and files in a file system. A user can expand and collapse rows, edit values, and resize and rearrange columns.
Which means that you can get a table looking hierarchical structure similar to a typical control used in many OS’s and application that allows display of a structure in a parent-child hierarchical relationship, with expandable and collapsible nodes that might contain sub nodes or end items:
In the example above – which is the structure of one of this article’s examples, revealed in Finder – the triangles can be clicked to expand or to collapse the corresponding folder, allowing the user to show or hide the contents of the folder. Such an hierarchical tree of items is usually called a TreeView widget. In Cocoa, this is what it is called NSOutlineView. Please note that if one removes the indent for each level in the hierarchical example above and also the expand/ collapse functionality, the result will be a one column table with a number of rows equal to the number of folders (nodes) and folder entries (children). Accordingly, NSOutlineView is a special case of a table, of NSTableView.
NSOutlineView data source
The idea behind NSOutlineView is that it gets its data from a data source. Rather than pure data, the data source is a class that conforms to a protocol for providing data items, i.e. specific methods mandatory (well… kind of; will discuss it in another post) to be implemented on order to provide and display the data items. You cannot feed directly a data structure, you have to create an object from this structure and use this object to populate the outline view.
The data source hands the items to the NSOutlineView. The data items themselves should be instances of a generic class that is derived from NSObject. Implementing the class for the data items might be harder than implementing the data source for the NSOutlineView.
A very simple example
Let’s create a very simple NSOutlineView example. We will use as data source a simple array of objects:
dataSource = [NSArray arrayWithObjects:@"John", @"Mary", @"George", nil];
In XCode Create a new Cocoa Application project. Open the automatically created AppDelegate.h file and set up an outlet NSOutlineView
object and an NSArray
data source object. Leave, for now, only the default protocol conformance of the class only to NSApplicationDelegate
:
#import @interface AppDelegate : NSObject { IBOutlet NSOutlineView *myOutlineView; NSArray *dataSource; } @property (assign) IBOutlet NSWindow *window; @end
Open the MainMenu.xib file and add an Outline View object to the Window object:
Connect the myOutlineView
outlet to the Outline View object in the window. In the AppDelegate.m file, create an awakeFromNib
method and initialize the data source:
- (void)awakeFromNib { dataSource = [NSArray arrayWithObjects:@"John", @"Mary", @"George", nil]; #if DEBUG NSLog(@"%@", dataSource); #endif }
Notice that I have included a simple NSLog
check that will be called only within the debug build of the app. This is also handy to prove the following point: you will be tempted to add the above code snippet in the applicationDidFinishLaunching
of the NSApplicationDelegate
method, created together with the project in AppDelegate.m. Don’t. Even if your data source will be initialized, nothing will show in the NSOutlineView
. The reason will be discussed in another post.
Let’s focus on providing the data for the NSOutlineView object. For this, we need to implement some needed data source delegate methods. For the moment, we will request conformance only for NSOutlineViewDataSource
protocol. Go to AppDelegate.h header and add conformance to this protocol in the @interface
section:
#import @interface AppDelegate : NSObject <NSApplicationDelegate, NSOutlineViewDataSource> { IBOutlet NSOutlineView *myOutlineView; NSArray *dataSource; } @property (assign) IBOutlet NSWindow *window; @end
We have to implement the following four methods in the AppDelegate.m class. Modify AppDelegate.m and add this code:
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { return [dataSource objectAtIndex:index]; } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { return NO; } - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { return [dataSource count]; } - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return item; }
Build and run the application. You should get something like this:
– (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
The outline view is asking here for a child of a particular item. Let’s think a bit about this in our data source context. Since our NSArray
data source has only atomic objects (John, Mary and George), all these array objects are, each, like “root” items. This is the simplest data source you might get: no hierarchical relationship between items. Taken individually, each object from the array has nothing hierarchically above and nothing hierarchically below. I.e. they have no “parent” items, and they have no “children” items. Basically we have nothing to check, we just have to return each object – i.e. item – from the array, one by one. This, of course, is based on the index provided as an NSInteger
argument in the method call Accordingly, in our very simple example, this method should return:
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { return [dataSource objectAtIndex:index]; }
– (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
The outline view asks here if the item is expandable. An item would be “expandable” if it has children, i.e. if contains some other items, or “subitems”. Since in our very simple example none of the dataSource
array has any subitems, this section of the code should return NO, no expandable item:
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { return NO; }
– (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
Here, the outline view wants to know how many rows should be created in order to display all objects from your data source. In this very simple example we should return here an NSInteger
equal to the number of objects in the dataSource
array, i.e. the “count” of the array objects:
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { return [dataSource count]; }
– (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
This is the most complex method by far, but for now we will keep it simple. The outline view wants to know from you what to display in each of the outline view columns. In our very simple example we have an outline view with just a column and we want to display each item, one after another. So we will return just the item:
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return item; }
Items, subitems and Data Source delegate methods
One of the most confusing things when working with these NSOutlineViewDataSource
Delegate protocol methods is to grasp the understanding on who the “item” is. Modify code in AppDelegate.m above and make the following method return directly the item:
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { return item; }
Build and run. You should get something like this:
Whoops ! What happened ?
Let’s get back to - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
method
As said the outline view is asking here for a child of a particular item. But what is the “item”? Well, it will soon turn out that the “item” depends on the context, that is why the object type of the argument is anonymous (id) since can be anything: an array, a dictionary, a string or an integer. For the scope of this specific method, the item is the data source itself, the dataSource array or the “root” object. Since we do not request here any children of this item, based on the index, the actual result looks like empty because we haven’t requested any children but just the root. In other words, we can test whether a specific item is a root entry if that specific item is nil. The “nil” designation is dependent on the NSOutlineView
class context and does not mean that the actual object is empty or void of any content, just that is a “root” object, within the outline view perspective.
Let’s prove this by a simple check. Modify the AppDelegate.m code again and make the following method look like this:
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { if (item == nil) { return YES; } return NO; }
You should get the following result:
This delegate method will make expandable each item that is nil. Or root item. Since we return three times for each “children” of the root the actual item, which is the root, the outline view will consider each entry in the list as being expandable. Moreover, since we instructed the outline view to display in the column the actual item:
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return item; }
the “look” of the outline view will be similar to an empty table with expandable rows since no row can hold within a tree representation of any root item by itself. However, if we instruct the outline view to display the string “boo” instead of the item:
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return @"boo"; }
you will get the following result:
Of course, clicking on the disclosure triangle will reveal nothing (and in some cases might crash your app quite spectacularly) because each entry in the outline view is, actually, nil – a root entry.
On the contrary, returning a child of the root instead of the item:
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { return [dataSource objectAtIndex:index]; }
will result in this:
because we instructed the outline view to display three times an non-nil (non-root) object, that has the value of “boo” and not the item:
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return @"boo"; }
Reverting in above to
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return item; }
will end in:
which is our desired result.
In my opinion, understanding NSOutlineViewDataSource Delegate protocol methods is the most difficult thing in mastering this cocoa UI component. As we will progress in more complex examples, you will find that it is also very powerful. That’s it for now. Try it for yourself with several different changes in the code and see what the results are. Next articles will build upon these basic steps.
Update January 24th, 2013
It is unbelievable for me to witness that this post ranks second in Google search for “NSOutlineView”. Frankly, I don’t understand how this happened because I haven’t put any effort in SEO and the time I have to update this blog is very limited. However, I think that — probably — there is a tremendous interest in the community for this class and for its subtleties. So I decided to put some more effort in completing the rest of the initially–intended series. Right now I am working on a series of articles on NSOutlineView inheritance and customization, usage of delegate methods and some advanced topics like optimization and debugging. However, this takes some time. In my opinion it does not make any sense to write something if you are not able to bring some improvements or clarity in the concepts already there on the Net or — at least — to help other programmers to have a better understanding on the underlying requirements and intricacies of some classes in order to be able to develop in a shorter amount of time and with less pains modern, useful and performant software. So I will bring my respect to all those that made this second on Google and will finish what I started. Thank you all.
Update May 24th, 2013
I created a code repository on Bitbucket to share code discussed in this blog. You can find it here: http://bitbucket.org/AlaudaProjects.
Hi, did you ever get round to posting the next articles?
Hi, Freerider,
I lack soooo much the time ! 🙂 I promise I will do the in a week or so.
Thank you for visiting and commenting.
Regards,
AP
Great! If you have any thoughts about using the “new” source list too that would be very helpful!
Actually I found this class one of the most difficult to grasp. My initial intention was to write a series on NSOutlineView implementation so, for sure, details on the updated class will follow. Thanks for your comments.
Thanks! This has been very helpfull, I can’t wait to see how to add more children data, reload the outlineView dynamically, and customize how data appears
Thank you, Emanuel, for visiting, reading and commenting. As said, given the interest this topic stirred, I will update the series. Stay tuned.
Regards, AP
Very nice and clear tutorial. Looking forward the next part 🙂 thank you!
Thank you, Szabolcs, for visiting, reading and commenting. Stay tuned. Next will come soon.
Regards,
AP
Second article in the series available here. Enjoy.
What I forgot to mention in my comment on the second part: There were some additional steps in part one I had to do in order to get it work under Xcode 4.6. After the first & run, there is only an empty outline view. I had to connect the NSOutlineView (INSIDE the NSScrollView, not the NSScrollView itself) to the App Delegate as “datasource”. Later on I had to change the identifier of both NSTableColumn to their according values.
I hope this helps everyone who stumbled there.
Hi, Awado, thanks for your comment. You’re right, there are some steps that I did not show in the tutorial. I was [wrongly] assuming that these are known — like second nature: connecting the outlets, data source etc. However, for the sake of completeness, I will update the article with these steps too. Much appreciated your feedback.
Regards,
AP
I’ve followed your tutorial to the letter, three or four times over, and the outline view is always empty. Perhaps your tutorial no longer works with XCode 5?
Hi, Kyle. Thank you for your comment. This sounds like the Outline View does not have its datasource connected. Are you sure you did this ? This tutorial is fairly simple and there were no such major changes to the underlying framework to make it obsolete. Recheck the outlet connections. Let me know, please.
Regards,
AP
Hi, Kyle, getting back on this: I have tried the project on Mavericks with Xcode 5.1.1. It works perfectly.
Regards,
AP