NSOutlineView Part1: Setting up an outlineView.

Xcode 8.0, macOS 10.11

(This is Part 1 of 3; in which we will create an NSOutlineView with a flexible data structure that can be used for simple trees and for structures like the one below which contain a number of different, unrelated items. Part 2 will deal with inserting and deleting items, and reacting to the user changing the selection. Part 3, eventually, will implement folders (custom grouping of items) and drag-and-drop reordering.)

NSOutlineView (and its stylish cousin, the SourceList) is a subclass of NSTableView that allows the display of a tree structure – you can collapse and expand items. I was inspired by http://www.knowstack.com/swift-source-list/ to create the data structure I use in this tutorial; sadly the example was written in Swift 2 and has enough awkwardness in its data structure that I decided to write my own rather than convert it.
In the past, I have used an NSTreeController and Cocoa Bindings with this. Much as I hate it, bindings seem to be on the way out. They’re an ObjectiveC technology – often very useful – which has never been ported to iOS, and the future seems to belong to DataSource/Delegate combos which need more boilerplate, but give you much more flexibility. So for this post, I will use the NSOutlineViewDelegate and NSOutlineViewDataSource protocols.
Below is the SourceList from an app called Storyist (version 2), clearly showing sections and subsections and the use of icons to make items more easily identifiable. Warning: NSOutlineView needs _a lot_ of setup to work.

ScreenShot2017-01-04at22.11.00-2017-01-4-21-58.png

Long setup is long
1) Create a new app, and drag a ‘SourceList’ into the main ViewController scene.

a) Create an outlet for it in the main ViewController

@IBOutlet weak var sourceList: NSOutlineView!

b) while you’re at it, declare that the ViewController will conform to NSOutlineViewDelegate and set it as the outlineView’s delegate in IB.

c) The cells already have identifiers: HeaderCell for the allCaps header cell, DataCell for the icon-and-label child cell. We’ll be using three types here here (Apple’s Interface Guidelines say ‘Avoid displaying more than two levels of hierarchy in a source list.’) In this example we’ll be using headers (Animal Kingdoms) and two levels of content (AnimalClass and Animal), so the first thing to do is to drag another NSTableCellView – this time, with a label only – into the outline view. I’ve set the labels on mine to use Optima as a font.
ScreenShot2017-01-07at20.18.08-2017-01-4-21-58.png
Set the identifiers to HeaderCell, IconCell and PlainCell respectively.

2) Set up the DataSource. Objects conforming to the NSOutlineViewDataSource protocol need to inherit from NSObject (if you don’t, you will get A LOT of errors telling you you need to conform to Equatable, Hashable, and thirteen further errors – they all go away if you make NSObject your DataSource’s superclass).

a) class CategoryDataSource: NSObject, NSOutlineViewDataSource {

var allKingdoms : [Kingdom] = [] }

This is going to be the content array. For this example, we will manually construct the contents in the NSOutlineViewDelegate (the main ViewController).

b) While you’re at it, go to IB, drag an object into the ViewController’s scene and set its class to CategoryDataSource; connect the OutlineView’s ‘dataSource’ outlet to that object. Also create an outlet for the dataSource in the main ViewController:

@IBOutlet var dataSource: CategoryDataSource!

At this point, you’ve got the outlineView hooked up to delegate and dataSource, and while neither of these classes actually conform to the protocol yet, you’re on your way.

2) Proof of concept: Single level.

There are three methods that need to be implemented in the NSOutlineViewDataSource. A fourth method is mentioned in older tutorials, but this seems to be mainly used by cell-based OutlineViews, and since they’ve been nearly-deprecated since 10.7, I’ve decided to leave it out for now. It does not seem to _do anything_ – the outlineView works perfectly fine without, though Apple’s documentation says you must implement this method if you are not providing the data for the outline view using Cocoa bindings.Unless I learn otherwise, I will leave this out. (I also struggle to understand what I am supposed to return here. I have seen examples that simply log which object is current, which seems to suggest the method was optional all along.)

func outlineView(_ outlineView: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? {
//code goes here
}

So we ignore that one.

a) Create a Kingdom class

class Kingdom {

var name: String = ""

init(name: String){
self.name = name
}
}

b) In the datasource, set allKingdoms to

var allKingdoms : [Kingdom] = (Kingdom(name: "Plants"), Kingdom(name: "Animals"), Kingdom(name: "Fungi")]

and implement a terrible implementation for the following three methods:

func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
return no
}

The top level – here, kingdoms – it not a child of any item, but we still need to say how many top level items there are, and return an item for the index value.

func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
return allKingdoms.count
}

func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
return allKingdoms[index] }

c) In the delegate (main ViewController), provide terrible implementations for the two necessary methods:

func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
return true
}

func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
sourceListCell = outlineView.make(withIdentifier: "HeaderCell", owner: self) as! NSTableCellView
sourceListCell.textField?.stringValue = item.name
return sourceListCell
}

They’re all group items (they’re all at the top level), and they’re all displayed in HeaderCells.

d) Build and run.
ScreenShot2017-01-07at20.52.13-2017-01-4-21-58.png

This is a good start. For the next part, we complicate everything. A lot.

3) Set up Data Structure

a) Create a class Animal, and a class AnimalClass.

b) Create a SourceListItem protocol. We want to be able to display many different items in our SourceList, including – as in the example before – custom ‘groups’ (folder icon), which allow us to split large sibling groups into more managable categories. So instead of creating endless switch statements (is this a Kingdom, an AnimalClass, an Animal, a Group folder, or something completely unrelated like a tag or a note or whatever else you might wish to display), we create a SourceListItem protocol that all classes you wish to display must conform to, and an easily expandable SourceListItemType enum to track the kinds of items we want to display.

protocol SourceListItem {

var name: String {get set }
var icon: NSImage? {get set}

var children: [SourceListItem] {get set}
var isExpandable: Bool { get }

var itemType: SourceListItemType {get set}

}

This should be fairly self-explanatory.

c) Create the SourceListType enum. The types are linked to how I want to display the item, rather than its content.

enum SourceListItemType {
case header
case plain
case icon
}

d) make the three model classes conform to SourceListItem.

class Kingdom: SourceListItem {

var name: String = ""
var children: [SourceListItem] = [] var icon: NSImage?

var isExpandable: Bool {
return !children.isEmpty
}

init(name: String){
self.name = name
}
var itemType: SourceListItemType = .header
}

Configure AnimalClass and Animal in the same manner, only that, for the purpose of this example, AnimalClass is a .plain and Animal an .icon itemType. (Alternatively, you could check whether icon is nil). You could also override the setter for children if you don’t want an element to ever have children of its own and disregard any attempt to set them.

d) change the dataSource methods:

var allKingdoms : [SourceListItem] = []

func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let item = item as? SourceListItem{
return item.isExpandable
} else {
return false
}
}

func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let item = item as? SourceListItem{
return item.children.count
}
return allKingdoms.count
}

func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let item = item as? SourceListItem{
return item.children[index] } else {
return allKingdoms[index] }
}

AllKingdoms can only ever contain elements that conform to our SourceListItem protocol, and we can easily find out whether items are expandable and how many children they have.

e) change the delegate methods:

func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
if let item = item as? SourceListItem {
if item.itemType == .header{
return true
} else {
return false
}
}
return false
}

.header type items are group items (which get displayed with the HeaderCell).

func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {

if let item = item as? SourceListItem {
var sourceListCell: NSTableCellView
switch item.itemType {

case .header:
sourceListCell = outlineView.make(withIdentifier: "HeaderCell", owner: self) as!NSTableCellView
sourceListCell.textField?.stringValue = item.name
return sourceListCell
case .plain:
sourceListCell = outlineView.make(withIdentifier: "PlainCell", owner: self) as!NSTableCellView
sourceListCell.textField?.stringValue = item.name
return sourceListCell
case .icon:
let sourceListCell = outlineView.make(withIdentifier: "IconCell", owner: self) as!NSTableCellView
sourceListCell.textField?.stringValue = item.name
sourceListCell.imageView?.image = item.icon
return sourceListCell
}

}
return nil
}

This isn’t best practice – I am setting the values of a view at creation here, when it would be better to set each NSTableCellView’s represented object and either bind to that in IB or subclass them and have the cell itself display its represented object, but for now, it will do.

f) add two (or more, if you want) images to your asset catalogue: here I’ve used ‘blackbird’ and ‘bee’.

g) create the data in viewDidLoad:

override func viewDidLoad() {
super.viewDidLoad()

let plants = Kingdom(name: "Plants")
let animals = Kingdom(name: "Animals")
let funghi = Kingdom(name: "Fungi")

let birds = AnimalClass(name: "Birds")
let mammals = AnimalClass(name: "Mammals")
let insects = AnimalClass(name: "Insects")

let blackbird = Animal(name: "Blackbird", icon: #imageLiteral(resourceName: "blackbird"))
let queenBee = Animal(name: "QueenBee", icon: #imageLiteral(resourceName: "bee"))
let bee1 = Animal(name: "Bee1", icon: #imageLiteral(resourceName: "bee"))
let bee2 = Animal(name: "Bee2", icon: #imageLiteral(resourceName: "bee"))
let bee3 = Animal(name: "Bee3", icon: #imageLiteral(resourceName: "bee"))
let bee4 = Animal(name: "Bee4", icon: #imageLiteral(resourceName: "bee"))

queenBee.children.append(bee1)
queenBee.children.append(bee2)
queenBee.children.append(bee3)
queenBee.children.append(bee4)

birds.children.append(blackbird)
insects.children.append(queenBee)

animals.children = [birds, mammals, insects]

dataSource.allKingdoms = [plants, animals, funghi]

//changed the content; reload
sourceList.reloadData()

}

h) build and run.

And there you go – groups and data cells are distinguishable, and you can nest your data to your heart’s content, or at least until you run out of space.

ScreenShot2017-01-07at21.21.20-2017-01-4-21-58.png

In itself, this isn’t useful yet – while you can display the content (and explore tree data visually), you cannot change it, add or remove objects from the tree, or do anything when any of the items are selected.

Since that’s quite a lot of work, I am leaving all of these for the second part of the outline view tutorial.