macOS NSTableView Tutorial

Table views are one of the most important macOS UI controls. Get up to speed with how to use them with this macOS NSTableView tutorial. By Warren Burton.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Playing With Columns in a Table View

By default, Interface Builder creates a table view with two columns, but you need three columns to display name, date and size file information.

Go back to Main.storyboard.

Select the table view in the View Controller Scene. Make sure that you select the table view and not the scroll view that contains it.

table in the view hierarchy

Open the Attributes Inspector. Change the number of Columns to 3. It’s as simple as that! Your table view now has three columns.

Next, check the Multiple checkbox in the Selection section, because you want to select multiple files at once. Also check Alternating Rows in the Highlight section. When enabled, this tells the table view to use alternating row colors for its background, just like Finder.

configure table attributes

Rename the column headers so the text is more descriptive. Select the first column in the View Controller Scene.

configure-column

Open the Attributes Inspector and change the column Title to Name.

change the header title in attributes

Repeat the operation for the second and third column, changing the Title to Modification Date and Size, respectively.

Note: There is an alternative method for changing the column title. You can double-click directly on the header on the table view to make it editable. Both ways have exactly the same end result, so go with whichever method you prefer.

Last, if you can’t see the Size column yet, select the Modification Date column and resize to 200. It beats fishing around for the resize handle with your mouse. :]

resize modification date if needed

Build and run. Here’s what you should see:

table with configured headers

Changing How Information is Represented

In its current state, the table view has three columns, each containing a cell view that shows text in a text field.

But it’s kind of bland, so spice it up by showing the icon of the file next to the file name. Your table will look much cleaner after this little upgrade.

You need to replace the cell view in the first column with a new cell type that contains an image and a text field.

You’re in luck because Interface Builder has this type of cell built in.

Select the Table Cell View in the Name column and delete it.

delete this view

Open the Object Library and drag and drop an Image & Text Table Cell View into either the first column of the table view or the View Controller Scene tree, just under the Name table view column.

add a new cell type in interface builder

Now you’re whipping things into shape!

Assigning Identifiers

Every cell type needs an assigned identifier. Otherwise, you’ll be unable to create a cell view that corresponds to a specific column when you’re coding.

Select the cell view in the first column, and in the Identity Inspector change the Identifier to NameCellID.

edit column identifiers

Repeat the process for the cell views in the second and third columns, naming the identifiers DateCellID and SizeCellID respectively.

Populating the Table View

Note: There are two ways that you can populate a tableview, either using the datasource and delegate protocols you’ll see in this macOS NSTableView tutorial, or via Cocoa bindings. The two techniques are not mutually exclusive and you may use them together at times to get what you want.

The table view currently knows nothing about the data you need to show or how to display it, but it does need to be looped in! So, you’ll implement these two protocols to provide that information:

  • NSTableViewDataSource: tells the table view how many rows it needs to represent.
  • NSTableViewDelegate: provides the view cell that will be displayed for a specific row and column.

population flow of a table view

The visualization process is a collaboration between the table view, delegate and data source:

  1. The table view calls the data source method numberOfRows(in:) that returns the number of rows the table will display.
  2. The table view calls the delegate method tableView(_:viewFor:row:) for every row and column. The delegate creates the view for that position, populates it with the appropriate data, and then returns it to the table view.

Both methods must be implemented in order to show your data in the table view.

Open ViewController.swift in the Assistant editor and Control-drag from the table view into the ViewController class implementation to insert an outlet.

add an outlet to the tableview

Make sure that the Type is NSTableView and the Connection is Outlet. Name the outlet tableView.

define the outlet

You can now refer to the table view in code using this outlet.

Switch back to the Standard Editor and open ViewController.swift. Implement the required data source method in the ViewController by adding this code at the end of the class:

extension ViewController: NSTableViewDataSource {
  
  func numberOfRows(in tableView: NSTableView) -> Int {
    return directoryItems?.count ?? 0
  }

}

This creates an extension that conforms to the NSTableViewDataSource protocol and implements the required method numberOfRows(in:) to return the number files in the directory, which is the size of the directoryItems array.

Now you need to implement the delegate. Add the following extension at the end of ViewController.swift:

extension ViewController: NSTableViewDelegate {

  fileprivate enum CellIdentifiers {
    static let NameCell = "NameCellID"
    static let DateCell = "DateCellID"
    static let SizeCell = "SizeCellID"
  }

  func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {

    var image: NSImage?
    var text: String = ""
    var cellIdentifier: String = ""

    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .long
    dateFormatter.timeStyle = .long
    
    // 1
    guard let item = directoryItems?[row] else {
      return nil
    }

    // 2
    if tableColumn == tableView.tableColumns[0] {
      image = item.icon
      text = item.name
      cellIdentifier = CellIdentifiers.NameCell
    } else if tableColumn == tableView.tableColumns[1] {
      text = dateFormatter.string(from: item.date)
      cellIdentifier = CellIdentifiers.DateCell
    } else if tableColumn == tableView.tableColumns[2] {
      text = item.isFolder ? "--" : sizeFormatter.string(fromByteCount: item.size)
      cellIdentifier = CellIdentifiers.SizeCell
    }

    // 3
    if let cell = tableView.make(withIdentifier: cellIdentifier, owner: nil) as? NSTableCellView {
      cell.textField?.stringValue = text
      cell.imageView?.image = image ?? nil
      return cell
    }
    return nil
  }

}

This code declares an extension that conforms to the NSTableViewDelegate protocol and implements the method tableView(_:viewFor:row). It’s then called by the table view for every row and column to get the appropriate cell.

There’s a lot going on the method, so here’s a step-by-step breakdown:

  1. If there is no data to display, it returns no cells.
  2. Based on the column where the cell will display (Name, Date or Size), it sets the cell identifier, text and image.
  3. It gets a cell view by calling make(withIdentifier:owner:). This method creates or reuses a cell with that identifier. Then it fills it with the information provided in the previous step and returns it.

Next up, add this code inside viewDidLoad():

tableView.delegate = self
tableView.dataSource = self

Here you tell the table view that its data source and delegate will be the view controller.

The last step is to tell the table view to refresh the data when a new directory is selected.

First, add this method to the ViewController implementation:

func reloadFileList() {
  directoryItems = directory?.contentsOrderedBy(sortOrder, ascending: sortAscending)
  tableView.reloadData()
}

This helper method refreshes the file list.

First, it calls the directory method contentsOrderedBy(_:ascending) and returns a sorted array with the directory files. Then it calls the table view method reloadData() to tell it to refresh.

Note that you only need to call this method when a new directory is selected.

Go to the representedObject observer didSet, and replace this line of code:

print("Represented object: \(url)")

With this:

directory = Directory(folderURL: url)
reloadFileList()

You’ve just created an instance of Directory pointing to the folder URL, and it calls the reloadFileList() method to refresh the table view data.

Build and run.

Open a folder using the menu File > Open… or the Command+O keyboard shortcut and watch the magic happen! Now the table is full of contents from the folder you just selected. Resize the columns to see all the information about each file or folder.

your table now shows content

Nice job!