NSOutlineView on macOS Tutorial

Discover how to display and interact with hierarchical data on macOS with this NSOutlineView on macOS tutorial. By Jean-Pierre Distler.

Leave a rating/review
Save for later
Share

Update 9/30/16: This tutorial has been updated for Xcode 8 and Swift 3.

When writing applications, you often want to show data that has a list-like structure. For example, you might want to display a list of recipes. This can be easily done with a NSTableView. But what if you want to group the recipes by appetizer or main course? Now you have a problem, because table views have no sections. Your five-layer-dip recipe is right next to your linguine del mare recipe, which just won’t do!

Thankfully, NSOutlineView provides a lot more functionality. NSOutlineView is a common component in macOS applications, and is a subclass of NSTableView. Like a table view, it uses rows and columns to display its content; unlike a table view, it uses a hierarchical data structure.

To see an outline view in action, open Xcode with an existing project and have a look at the project navigator. Click on the triangle next to the project name to expand it. You’ll see a structure like the image below: beneath the project are groups, and inside the groups are Swift or Objective-C files.

Xcode's Outline View

In this NSOutlineView on macOS tutorial, you will learn how to use an outline view to show your own hierarchical data. To do this, you’ll write a RSS Reader-like application that loads RSS Feeds from a file and shows them in an outline view.

Getting Started

The starter project can be downloaded here. Open the project and take a peek. Besides the files created by the template, there is a Feeds.plist, which is the file you’ll load the feeds from. You’ll take a closer look at it later, when creating the model classes.

Open Main.storyboard to see the prepared UI. On the left is a plain outline view, and beside it is a white area, which is the web view. Those are grouped using a horizontal stack view, which is pinned to the window edges. Stack views are the latest and greatest way to deal with Auto Layout, if you haven’t yet given them a try. You can learn all about them in Marin’s great tutorial about NSStackViews.

Starter UI

Your first task: complete the UI. To do this, double-click in the header to change the title. For the first column, change it to Feed; change the second to Date.

Change_Header

That was easy! Now select the outline view, in the document outline — you’ll find it under Bordered Scroll View – Outline View \ Clip View \ Outline View. In the Attributes Inspector, change Indentation to 5, enable Floats Group Rows and disable Reordering.

nsoutline-inspector-new

Inside the document outline on the left, click on the triangle beside Outline View to expand it. Do the same for Feed and Date. Select the Table View Cell below Date.

Date_Selection

Change the Identifier to DateCell in Identity Inspector.

Change_Cell_Identifier

Now show the Size Inspector and change Width to 102. Repeat this step for the cell below Feed, changing the Identifier to FeedCell and Width to 320.

Expand the cell below feed and select the text field named Table View Cell.

Selected_Textfield

Use the Pin and Align menus on the Auto Layout toolbar to add an Auto Layout constraint of 2 points leading, plus another constraint to center the text field vertically. You will see the constraints in Size Inspector:

Constraints

Now select the table cell again (above the text field in the layout hierarchy). Duplicate it by pressing Cmd + C and Cmd + V, then change the Identifier of the duplicate to FeedItemCell. Now you have 3 different cells, one for each type of entry that will be shown in the outline view.

FinishedCells

Select Date, and in the Identity Inspector change the Identifier to DateColumn; do the same for Feed and change it to TitleColumn:

TitleColumn

The final step is to give the outline view a delegate and a data source. Select the outline view and right- or control-click on it. Drag a line from dataSource to the blue circle that represents your view controller; repeat this to set the delegate.

Add_Delegate

Run the project and you’ll see …

First_Run

There’s an empty outline view and an error message in your console, saying you have an illegal data source. What’s wrong?

Before you can fill the outline view and get rid of the error message, you need a data model.

Data Model

The data model for an outline view is a bit different than the one for a table view. Like mentioned in the introduction, an outline view shows a hierarchical data model, and your model classes have to represent this hierarchy. Every hierarchy has a top level or root object. Here this will be a RSS Feed; the name of the feed is the root.

Press Cmd + N to create a new class. Inside the macOS section select Cocoa Class and click Next.

New_File_Template

Name the class Feed and make it a subclass of NSObject. Then click Next and Create on the next screen.

Create_Feed_Class_2

Replace the automatically generated code with:

import Cocoa

class Feed: NSObject {
   let name: String
       
   init(name: String) {
     self.name = name
   }
}

This adds a name property to your class and provides an init method that sets the property to a provided value. Your class will store its children in an array, but before you can do this, you need to create a class for those children. Using the same procedure as before, add a new file for the FeedItem class. Open the newly created FeedItem.swift and replace the content with the following:

import Cocoa

class FeedItem: NSObject {
  let url: String
  let title: String
  let publishingDate: Date
	  
  init(dictionary: NSDictionary) {
    self.url = dictionary.object(forKey: "url") as! String
    self.title = dictionary.object(forKey: "title") as! String
    self.publishingDate = dictionary.object(forKey: "date") as! Date
  }
}

This is another simple model class: FeedItem has a url that you will use to load the corresponding article into the web view; a title; and a publishingDate. The initializer takes a dictionary as its parameter. This could be received from a web service or, in this case, from a plist file.

Head back to Feed.swift and add the following property to Feed:

var children = [FeedItem]()
 

This creates an empty array to store FeedItem objects.

Now add the following class method to Feed to load the plist:

class func feedList(_ fileName: String) -> [Feed] {
  //1
  var feeds = [Feed]()
    
  //2
  if let feedList = NSArray(contentsOfFile: fileName) as? [NSDictionary] {
    //3
    for feedItems in feedList {
      //4
      let feed = Feed(name: feedItems.object(forKey: "name") as! String)
      //5
      let items = feedItems.object(forKey: "items") as! [NSDictionary]
      //6
      for dict in items {
        //7
        let item = FeedItem(dictionary: dict)
        feed.children.append(item)
      }
      //8
      feeds.append(feed)
    }
  }
    
  //9
  return feeds
 }

The method gets a file name as its argument and returns an array of Feed objects. This code:

  1. Creates an empty Feed array.
  2. Tries to load an array of dictionaries from the file.
  3. If this worked, loops through the entries.
  4. The dictionary contains a key name that is used to inititalize Feed.
  5. The key items contains another array of dictionaries.
  6. Loops through the dictionaries.
  7. Initializes a FeedItem. This item is appended to the children array of the parent Feed.
  8. After the loop, every child for the Feed is added to the feeds array before the next Feed starts loading.
  9. Returns the feeds. If everything worked as expected, this array will contain 2 Feed objects.

Open ViewController.swift, and below the IBOutlet section add a property to store feeds:

var feeds = [Feed]()

Find viewDidLoad() and add the following:

if let filePath = Bundle.main.path(forResource: "Feeds", ofType: "plist") {
  feeds = Feed.feedList(filePath)
  print(feeds)
}

Run the project; you should see something like this in your console:

[<Reader.Feed: 0x600000045010>, <Reader.Feed: 0x6000000450d0>]

You can see that you’ve successfully loaded two Feed objects into the feeds property — yay!