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
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Finishing Touches

Your example application is now working, but there are at least two common behaviors missing: double-clicking to expand or collapse a group, and the ability to remove an entry from the outline view.

Let’s start with the double-click feature. Open the Assistant Editor by pressing Alt + Cmd + Enter. Open Main.storyboard in the left part of the window, and ViewController.swift in the right part.

Right-click on the outline view inside the Document Outline on the left. Inside the appearing pop-up, find doubleAction and click the small circle to its right.

AssistantEditor

Drag from the circle inside ViewController.swift and add an IBAction named doubleClickedItem. Make sure that the sender is of type NSOutlineView and not AnyObject.

AddAction

Switch back to the Standard editor (Cmd + Enter) and open ViewController.swift. Add the following code to the action you just created.

@IBAction func doubleClickedItem(_ sender: NSOutlineView) {
  //1
  let item = sender.item(atRow: sender.clickedRow)
 
  //2
  if item is Feed {
    //3
    if sender.isItemExpanded(item) {
      sender.collapseItem(item)
    } else {
      sender.expandItem(item)
    }
  }
}

This code:

  1. Gets the clicked item.
  2. Checks whether this item is a Feed, which is the only item that can be expanded or collapsed.
  3. If the item is a Feed, asks the outline view if the item is expanded or collapsed, and calls the appropriate method.

Build your project, then double-click a feed. It works!

The last behavior we want to implement is allowing the user to press backspace to delete the selected feed or article.

Still inside ViewController.swift, add the following method to your ViewController. Make sure to add it to the normal declaration and not inside an extension, because the method has nothing to do with the delegate or datasource protocols.

override func keyDown(with theEvent: NSEvent) {
  interpretKeyEvents([theEvent])
}

This method is called every time a key is pressed, and asks the system which key was pressed. For some keys, the system will call a corresponding action. The method called for the backspace key is deleteBackward(_:).

Add the method below keyDown(_:):

override func deleteBackward(_ sender: Any?) {
  //1
  let selectedRow = outlineView.selectedRow
  if selectedRow == -1 {
    return
  }
   	
  //2
  outlineView.beginUpdates()

  outlineView.endUpdates()
}
  1. The first thing this does is see if there is something selected. If nothing is selected, selectedRow will have a value of -1 and you return from this method.
  2. Otherwise, it tells the outline view that there will be updates on it and when these updates are done.

Now add the following between beginUpdates() and endUpdates():

//3
if let item = outlineView.item(atRow: selectedRow) {
     
  //4
  if let item = item as? Feed {
    //5
    if let index = self.feeds.index( where: {$0.name == item.name} ) {
      //6
      self.feeds.remove(at: index)
      //7
      outlineView.removeItems(at: IndexSet(integer: selectedRow), inParent: nil, withAnimation: .slideLeft)
    }
  }
}

This code:

  1. Gets the selected item.
  2. Checks if it is a Feed or a FeedItem.
  3. If it is a Feed, searches the index of it inside the feeds array.
  4. If found, removes it from the array.
  5. Removes the row for this entry from the outline view with a small animation.

To finish this method, add the code to handle FeedItems as an else part to if let item = item as? Feed:

		
else if let item = item as? FeedItem {
  //8
  for feed in self.feeds {
    //9
    if let index = feed.children.index( where: {$0.title == item.title} ) {
      feed.children.remove(at: index)
      outlineView.removeItems(at: IndexSet(integer: index), inParent: feed, withAnimation: .slideLeft)
    }
  }
}
  1. This code is similar to the code for a Feed. The only additional step is that here it iterates over all feeds, because you don’t know to which Feed the FeedItem belongs.
  2. For each Feed, the code checks if you can find a FeedItem in its children array. If so, it deletes it from the array and from the outline view.

Note: Not only can you delete a row, but you can also add and move rows. The steps are the same: add an item to your data model and call insertItemsAtIndexes(_:, inParent:, withAnimation:) to insert items, or moveItemAtIndex(_:, inParent:, toIndex:, inParent:) to move items. Make sure that your datasource is also changed accordingly.

Now your app is complete! Build and run to check out the new functionality you just added. Select a feed item and hit the delete key–it’ll disappear as expected. Check that the same is true for the feed as well.

Where To Go From here?

Congrats! You’ve created an RSS Feed Reader-type app with hierarchical functionality that allows the user to delete rows at will and to double-click to expand and collapse the lists.

You can download the final project here.

In this NSOutlineView on macOS tutorial you learned a lot about NSOutlineView. You learned:

  • How to hook up an NSOutlineView in Interface Builder.
  • How to populate it with data.
  • How to expand/collapse items.
  • How to remove entries.
  • How to respond to user interactions.

There is lots of functionality that you didn’t get chance to cover here, like support for drag and drop or data models with a deeper hierarchy, so if you want to learn more about NSOutlineView, take a look at the documentation. Since it is a subclass of NSTableView, Ernesto García’s tutorial about table views is also worth a look.

I hope you enjoyed this NSOutlineView on macOS tutorial! If you have any questions or comments, feel free to join the forum discussion below.