In this post I’ll walk through setting up a hierarchical view with expanding nodes using NSOutlineView and NSTreeController. This is the sort of thing you’d want if you had to describe a file system. In this example I’ll be using Xcode 7 and Swift 2.
Create a Node class
First we’ll create a data source, or model. Instead of constructing a tree ourselves, we will describe a tree node, and then NSTreeController will use that tree node to manage our tree. At the very least a tree node needs to know about its children, so we’ll need to have a children attribute in our tree node that holds more tree nodes.
Create a new Cocoa application. Select AppDelegate.swift
and press ⌘N to create a new Swift file. Name it Node.swift
and add the following code,
import Cocoa class Node: NSObject { let data: String var children: [Node] = [] init( data: String ) { self.data = data } func isLeaf() -> Bool { if children.isEmpty { return true } else { return false } } }
Create a Tree in AppDelegate
Next we’ll add the lines 8 through 19 to our AppDelegate.swift
file. This will create an instance of the Node
class called content
. We’ll then add a root node with two children. We’ll add children (clumsily) by accessing the children
attribute of the the root node, and then using the append
method to add new Node
objects to the root.
import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var window: NSWindow! var content: [Node] = [] @IBOutlet var treeController: NSTreeController! override func awakeFromNib() { if self.content.count == 0 { var aNode: Node! aNode = Node( data: "Parent" ) aNode.children.append( Node( data: "Child1" ) ) aNode.children.append( Node( data: "Child2" ) ) self.treeController.addObject( aNode ) } } func applicationDidFinishLaunching(aNotification: NSNotification) { // Insert code here to initialize your application } func applicationWillTerminate(aNotification: NSNotification) { // Insert code here to tear down your application } }
Create the User Interface and Bindings
First bring up the Interface Builder by clicking on MainMenu.xib
in the Navigator area. Drag the Tree Controller from the Object Library to the Document Outline to the left of the Canvas.
Next, drag an Outline View from the Object Library to the window in the Canvas. At this point, your window should look something like this,
Select the Tree Controller icon in the Document Outline on the left, and then bring up the Attributes Inspector in the Utilities pane on the right. Under the Key Paths section, set Children to children, and Leaf to isLeaf.
At this point (because it’s as good a time as any) you can delete one of the Table Column objects in the Document Outline (highlighted below) and resize the Outline View in the Canvas a little bit to display only one column in the window.
Select the Outline View in the Document Outline on the left, and then bring up the Bindings Inspector in the Utilities pane on the right. Under Outline View Content, bind the Outline View to the Tree Controller using the checkbox.
With the Bindings Inspector in the Utilities pane on the right still open, select the Table View Cell in the Document Outline on the left. Under the Value section, bind the Table View Cell to the Table Cell View (not joking) using the checkbox. Also, change objectValue
under Model Key Path to objectValue.data
. This will connect the data attribute of the Node class to the Outline View.
Finally, connect the Delegate to the Tree Controller. Select the Delegate in the Document Outline on the left. Looking at the Connections Inspector in the Utilities pane on the right, CTRL-drag the empty circle next to treeController under the Outlets section across the screen to the Tree Controller in the Document Outline. After that, your screen should look something like this.
And that’s it! You should be able to hit ⌘R to build and run the project and see a window with an expandable outline view!
References
I learned a ton from a Big Nerd Ranch bootcamp taught by Mikey Ward I was fortunate enough to attend last week. These guys are really amazing–they put in 12+ hour days and they’re sharp as a tack all the way through. I also used three books to figure all this stuff out,
- Big Nerd Ranch’s Advanced Mac OS X Programming
- Big Nerd Ranch’s Cocoa Programming for OS X
- O’Reilly’s Swift Development with Cocoa
Thanks for sharing this! It’s helped a lot.
One question though; since you’re using bindings, shouldn’t
self.treeController.addObject( aNode )
be removed and instead you just populate your content array? e.g.
content.append( aNode )
I have to admit, I did this, and got an empty source list, and can’t make it work :p Is my theory wrong?
Aha, got it working! 🙂
I added the
dynamic
keyword to the bound array:dynamic var content: [Node] = []
Then I could delete the line:
self.treeController.addObject( aNode )
replacing it with:
content.append( aNode )
Hi, Connor. Excellent post. Outline is a great view, but it’s overshadowed a bit by all the tutorials on tableviews, so it’s nice to see a well-explained tutorial.
@Jeff: in case anyone is following your solution, you forgot to mention you also need to bind the tree controller’s content array to the content array variable. (Connor put that in his last screenshot, but didn’t mention it in his text, so probably he was half-thinking along the same lines). When you do that, it’s not necessary to have a delegate outlet to the tree controller. You also need to set the class name to Node and tick prepares content.
So to summarise Connor’s bindings plus Jeff’s idea:
Outline content binds to tree controller (a.o.)
Tree controller content array binds to delegate
(and of course the cell view/ “no joking” malarky binds to node.data, what a ridiculous notation from Apple that is).