Kit Cross

Using NSTableView inside Xcode Playgrounds

Using XCode Playgrounds makes testing UI elements easy and fast. It’s not obvious how to add an NSTableView without using a NIB – here’s how to create a table view programatically.

Playgrounds were introduced in Xcode 7 and they are a great way to quickly test ideas. At first they were non-interactive, but since they now allow interaction I often use them to test table views and other complex controls.

The venerable NSTableView shares plenty of code with its cousin in UIKit but has a few more quirks due to its advanced age. With UIKit, adding a table view to a playground is easy. We just drag out a UITableViewController, subclass it and override the methods to return the number of items we need. However, there is no NSTableViewController equivalent we can use in AppKit. And while loading NIB files inside a Playground is possible, you have to compile the NIB before the Playground can read it.

Thus I find it quicker to create the table view programmatically. And at the very least, it gives a good understanding of the moving parts of a table view and how they fit together.

Behind the scenes of NSTableView

It helps to understand how a table view is generated. The Xcode visual debugger shows the layers that make up a table view. These are automatically generated when you drag out a new table view in Interface Builder.

An exploded NSTableView shows the five layers that Interface Builder automatically generates

From left to right the relevant views are the NScrollView in blue, two views which make up the content background, then NSClipView and NSTableView.

An NSScrollView handles user scrolling along the axis that we need. It provides the scrubber and horizontal and vertical scrolling tracks.

The NSClipView ensures that the document view of the scroll view is clipped to its bounds. It also draws the over and underscroll background and hence AppKit has added the second and third layers.

Finally, our NSTableView class is the last in the view stack and draws the rows, the table header and footer and all the subviews.

Creating a table view controller

We’ll create a new view controller and programatically assign the table view. Because the table view requires both a scroll view and the table view, we need to create both inside our view controller.

As a table view is normally instantiated from a NIB file, there are a few quirks that we should take care of when creating from code. Normally a table has a header view. We don’t want one, so we’ll set it nil.

A table view defaults to setting the intercell spacing (the gaps between columns and rows) to (3.0, 2.0). This table view only has one column, so setting the intercell spacing to zero will prevent the column from being 3 points wider than we want.

import AppKit
import PlaygroundSupport

let tableViewFrame = NSRect(x: 0, y: 0, width: 250, height: 500)

class TableViewController: NSViewController {
  let tableView: NSTableView = {
    let tableView = NSTableView()

    // By default a tableview has column spacing. We only have
    // one column so this will prevent table side scrolling
    tableView.intercellSpacing = NSSize(width: 0, height: 2)
    tableView.headerView = nil
    return tableView
  }()

  let scrollView = NSScrollView()
  let column = NSTableColumn()

  override func viewDidLoad() {
    super.viewDidLoad()

    tableView.dataSource = self
    tableView.delegate = self
    tableView.addTableColumn(column)

    column.width = tableViewFrame.size.width

    scrollView.documentView = tableView
  }

  // Loading a view controller without a NIB requires that we
  // override loadView and provide our own view instead
  override func loadView() {
    view = scrollView
  }
}

A few more quirks to be aware of. Notice that we have to create and add a table column manually inside viewDidLoad. If we don’t do this, the delegate will never call the viewFor tableColumn method and nothing will be drawn. We also have to tell the scroll view about the table view, by setting the documentView to our table view.

Delegate and data source

Our view controller will also serve as the NSTableViewDataSource and NSTableViewDelegate. I typically place these delegate methods in their own extensions, but it doesn’t matter where they go.

extension TableViewController: NSTableViewDataSource {
  func numberOfRows(in tableView: NSTableView) -> Int {
    return 30
  }
}

extension TableViewController: NSTableViewDelegate {
  func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
    return 20
  }

  func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    return NSTextField(labelWithString: "Cell \(row + 1)")
  }
}

This is the bare minimum to get a functioning table view. We set the number of rows and return a textfield as we want this to be a view based table.

I wrote this inside a playground, to preview the result we need to assign the table view to the current live view.

let vc = TableViewController(nibName: nil, bundle: nil)
vc.view.frame = tableViewFrame

PlaygroundPage.current.liveView = vc

And that’s all there is to it, a live table view inside a Playground. Have fun learning by hacking around with table views.

A live table view inside a Xcode playground

Download the example

I’ve created a gist that shows how this all fits together.

swift cocoa macos