Introduction
Outline views are great for displaying hierarchical data because they allow the user to drill down into the parts of the dataset that are the most interesting.
A common place where they show up is in file handling windows. So. My last example in this column will be a directory browser. To build an outline view, all you have to do is create the view in Interface Builder, hook it up to your code and implement 4 specific delegate methods. Simple really.
It turns out that unless you try and understand some of what NSOutlineView does behind the scenes, the chances of actually hooking up your data right are remote… If you have a bug, what tends to happen is that you either get an exception/crash or nothing happens. And since much of the inner workings take place beneath the hood inside runtime, it’s not as if you can put a breakpoint inside the NSOutlineView to see what’s going on. It can take many NSLogs in your code to get some visibility.
Delegate Methods
You can think of delegate methods like the outsourcing of a task. When the outline view needs to know more about the data that you would like to display in the view, it simply calls the relevant “delegate” method and expects an answer.
These methods are the interface between your dataset and the onscreen view. At a minimum, NSOutlineView requires you to implement 4 delegate methods in your code. There are several other delegates that are strictly optional and which add more functionality to your outline view.
It’s easier to call the 4 compulsory delegates Method 1-4 instead of their full names; besides, this is the order in which runtime tends to call them anyway, so it’s a useful frame of reference.
Let’s dig in to how it all works.
Method#1
– (int) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem:(id)item
This is the first delegate that runtime will call to populate your outline view. It asks the question: “For the node that you just clicked on to expand, how many children objects should I allow for?”
This method is only called on one of 2 occasions. Either:
- When the view is first displayed at start up and runtime needs to know the contents of the Root level of your data
- When a node is expanded and runtime needs to start building the display of that node’s children.
Since these cases both happen relatively rarely, Method#1 tends to be called the least frequently out of the 4 delegates.
The next delegate methods #2 and #3 will be called the number of times you specify in the number you return now. So, if you return “3” here, they will get called 3 times.
2 values are passed to this delegate by runtime:
- (NSOutlineView *) outlineView (or whatever you choose to name the view). This pointer is passed to you in all 4 delegates so I’m only going to bother to explain it once here. When you build an outline view in Interface Builder, you have the option of making a multi-column display. You could have 2 different outlines displaying in each column. To let you know which outlineview runtime is talking about, it sends you a pointer. For a simple 1 column outlineview, you can ignore this pointer.
- (id) item. This is a pointer to the object associated with the node that the user clicked on. For example, in the above diagram, when the user clicked on “Applications”, the pointer to item here would point to the object associated with Applications. There is one exception to this case and that’s at start up. When the view first displays data, runtime doesn’t know where the data that you want to display is, so it will pass you a null pointer in item. You can test for null to know when you need to return root level data. That’s what many implementations do.
So far so good I hope.
Method#2
– (id) outlineView: (NSOutlineView *) outlineView child:(int) index ofItem:(id) item
I find this is the most tricky delegate. 9 times out of 10 getting my data right for this delegate is the source of my NSOutlineView bugs.
This method says (for example) “In Method#1 you told me the node had 5 children. Give me now a pointer to the data object that you want to be associated with (say) child#3”
You return a pointer to the object that for that child.
Method#2 repeats this loop once for every child that you declared in your return from Method#1. So, if you returned “5” from Method#1, Method#2 will loop 5 times.
It is important to note that each child object of a node must be unique. In that way it’s like an NSDictionary key where each key can only appear once. This is obviously to make sure that there is no ambiguity as to which data object applies to which node in the view. If you do not return a unique object, weird and wonderful bugs will occur that may show up immediately or later after much lurking.
All you need to do is return a pointer to the object that you want to associate with the child node that the method is asking about.
Method#2 delegate passes you 3 pointers:
- An outlineView pointer as in Method#1.
- (NSInteger) index. The number of the child under the node that is in question. If your data is stored in an array, then this integer can be used to reference sequential objects in that array.
- (id) item. A pointer to the object representing the node in question. If we are dealing with Root, then again this will be null as in Method#1. This is the place where you return a pointer to the data object(s) that will represent the root data items. I show an example of this in the directory browser at the end.
Method#3
– (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
Easy method. Simply asks “Is the data object that you returned in Method#2 expandable?” If it is, a disclosure triangle is drawn.
We have seen before both of the pointers that are sent to you. One thing to note is that the (id)item sent here may be different than the one in the prior Method#2. Assuming that they will be the same will be at your own great peril… that’s a hard bug to find (been there). Always trap it and check which item is being requested.
All you need to do is return a Boolean of yes or no.
Method#4
– (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
Last method. Also easy.
“For the object pointed to by item, give me the data that you actually want displayed in the view.”
Subtle difference to Method#2. Method#2 asks you for a pointer to the object to associate with a node. Method#4 asks you for the actual data you want to display. That data doesn’t necessarily need to come from the Method#2 object as I will show in a minute. It could be any text, image or whatever you like. Equally, it could be a member of the object that you supplied to Method#2.
Overall Flow and Performance
Now the we have covered the delegate methods, let’s delve into what goes on under the hood.
Delegates tend to be called in the order shown in the flowchart.
At the start, Method#1 is called for Root. Methods 2 and 3 are then called in pairs, once for each child node that was returned from Method#1 until the view is full. This is an important point. Runtime uses resources wisely and only asks about nodes for which there is space to display in the view. This saves you a bunch of effort since runtime figures out which nodes should be on the screen and which ones have scrolled out of view. The smaller the view, the lower the number of nodes that will fit in it. Nodes lower down only get called up when the user scrolls down in the view. If Root has 10 children but there is only room for 5 in the view, then Methods 2&3 will get called 5 times each.
Finally, Method#4 gets called once per node in the view. This method also gets called every time that the view needs to be redrawn, for example when the view’s window gets moved or when it gets revealed from behind another window.
When a node is expanded, things happen in a similar fashion, with Method#1 being called first for the expanded node to find out how many children it has.
As an experiment, I ran the directory demo from the end of this blog a few times and logged how many times each delegate method was called as I scrolled around and expanded random nodes. Averaging out the call stats and normalizing relative to Method#1 gave the below.
Apple’s doc makes the point that the 4 delegates get called a lot and that you should therefore take care not to bog them down with a lot of code. This is true, but clearly not all delegates are equal and some are called way more than others.
The performance of your delegate code matters, and it especially matters in Method#4.
Method#1 only gets called at start up and when a node is expanded. So, if you can move processing tasks from Methods 2-4, here is a good place to put it.
Method#4 gets called to set up the initial display of every item on the screen and then for every refresh. So, every time you move the view’s window, you resize it or that you cover the view up, Method#4 gets called a bunch of times to refresh the view. Therefore, your mileage will vary and the number of Method#4‘s that you get will too. In my experiment it was called ~4x more than Methods 2 & 3. So, it pays to keep the code in Method#4 as simple and as fast as possible.
Enough of the intro to NSOutlineView. Next time, the world’s simplest example code…