macOS NSTableView Tutorial

Warren Burton

Update note: This macOS NSTableView tutorial has been updated to Xcode 8 and Swift 3 by Warren Burton. The original tutorial was written by Ernesto García.

Create awesome user interfaces with table views!

Create awesome user interfaces with table views!

Table views are one of the most ubiquitous controls in macOS applications, with familiar examples being Mail’s message list and Spotlight’s search results. They allow your Mac to represent tabular data in an attractive way.

NSTableView arranges data in rows and columns. Each row represents a single model object within a given data collection, and each column displays a specific attribute of a model object.

In this macOS NSTableView tutorial, you’ll use a table view to create a functional file viewer that will bear a striking resemblance to Finder. As you work through it, you’ll learn a lot about table views, such as:

  • How to populate a table view.
  • How to change its visual style.
  • How to react to user interaction, like a selection or double-click.

Ready to create your first table view? Read on!

Getting Started

Download the starter project and open it in Xcode.

Build and run to see what you’re starting with:

window at start of tutorial

You have a blank canvas from which you’ll create a cool file viewer. The starter app already has some of the functionality you’ll need to work through this tutorial.

With the application open, choose File > Open… (or use the Command+O keyboard shortcut).

open a folder

From the new window that pops up, choose any folder you want and click the Open button. You’ll see something like this in Xcode’s console:

Represented object: file:///Users/tutorials/FileViewer/FileViewer/

This message shows the selected folder’s path, and the code in the starter project passes that URL to the view controller.

If you’re curious and want to learn more about how things are implemented, here’s where you should look:

  • Directory.swift: Contains the implementation of the Directory struct that reads the content of a directory.
  • WindowController.swift: Contains the code that presents you with the folder selection panel and passes the selected directory to the ViewController.
  • ViewController.swift: Contains the implementation of the ViewController class and is where you’ll spend some time today. It’s where you’ll create the table view and show the file list.

Creating the Table View

Open Main.storyboard in the Project Navigator. Select the View Controller Scene and drop a table view from the Object Library into the view. There’s a container in the view hierachy named Table Container all ready for you.

add a table in interface builder

Next, you need to add some constraints. Click the Pin button in the Auto Layout toolbar. In the popup that appears, set the all the edge constraints as follows:

  • Top, Bottom, Leading and Trailing: 0.

constrain the table to its container

Be sure to set Update Frames to Items of New Constraints, then click Add 4 Constraints.

Take a moment to have a look at the structure of a newly created table view. As you probably gathered from its name, it follows typical table structuring:

  • It’s made up of rows and columns.
  • Each row represents a single item within the data model collection.
  • Each column displays specific attributes of the model.
  • Each column can also have a header row.
  • Header rows describe the data within a column.

table view inner structure

If you’re familiar with UITableView on iOS, you’re treading familiar waters, but they’re much deeper here in macOS. In fact, you might be surprised by the number of individual UI objects in the object hierarchy that make up an NSTableView.

NSTableView is an older and more complex control than a UITableView, and it serves a different user interface paradigm, specifically, where the user has a mouse or trackpad.

The main difference with UITableView is that you have the possibility of multiple columns and a header that can be used to interact with the table view, for example, ordering and selecting.

Together, NSScrollView and NSClipView, respectively scroll and clip the contents of the NSTableView.

There are two NSScroller objects — one each for vertical and horizontal scrolling across the table.

There are also a number of column objects. An NSTableView has columns, and these columns have headers with titles. It’s important to note that users can resize and reorder columns, though you have the power to remove this ability by setting its default to disabled.

Anatomy of NSTableView

In Interface Builder, you’ve seen the complexity of the view hierarchy of the table view. Multiple classes cooperate to build the table structure, which usually ends up looking like this:

components of a table view

These are the key parts of an NSTableView:

  • Header View: The header view is an instance of NSTableHeaderView. It’s responsible for drawing the headers at top of the table. If you need to display a custom header, you can use your own header subclasses.
  • Row View: The row view displays the visual attributes associated with every row in the table view, like a selection highlight. Each row displayed in the table has its own instance of the row view. An important distinction to make is that rows do not represent your data; that the cell’s responsibility. It only handles visual attributes like selection color or separators. You can create new row subclasses to style your table view differently.
  • Cell Views: The cell is arguably the most important object in a table view. At the intersection of a row and column, you find a cell. Each one is an NSView or NSTableCellView subclass, and its responsibility is to display the actual data. And guess what? You can create custom cell view classes to display the content however you’d like.
  • Column: The columns are represented by the NSTableViewColumn class, which is responsible for managing width and behavior of the column, such as resizing and repositioning. This class is not a view, but a controller class. You use it to specify how columns should behave, but you don’t control the visual styles of the columns with it because the header, row and cell views have got things covered.

Note: There are two modes of NSTableView. The first is a cell-based table view called an NSCell. It’s like an NSView, but older and lighter. It comes from earlier days of computing when the desktop needed optimizations in order to draw controls with minimal overhead.

Apple recommends using view-based table views, but you’ll see NSCell in many of the controls in AppKit, so it’s worth knowing what it is and where it comes from. You can read more about NSCell in Apple’s Control and Cell Programming Topics

Well, now that was a nice little jog into the basic theory behind table view structure. Now that you’ve had all that, it’s time to go back to Xcode and get to work on your very own table view.

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!

Table View Interaction

In this section, you’ll work with some interactions to improve the UI.

Responding to User Selection

When the user selects one or more files, the application should update the information in the bottom bar to show the total number of files in the folder and how many are selected.

In order to be notified when the selection changes in the table view, you need to implement tableViewSelectionDidChange(_:) in the delegate. This method will be called by the table view when it detects a change in the selection.

Add this code to the ViewController implementation:

func updateStatus() {
 
  let text: String
 
  // 1
  let itemsSelected = tableView.selectedRowIndexes.count
 
  // 2
  if (directoryItems == nil) {
    text = "No Items"
  }
  else if(itemsSelected == 0) {
    text = "\(directoryItems!.count) items"
  }
  else {
    text = "\(itemsSelected) of \(directoryItems!.count) selected"
  }
  // 3
  statusLabel.stringValue = text
}

This method updates the status label text based on the user selection.

  1. The table view property selectedRowIndexes contains the indexes of the selected rows. To know how many items are selected, it just gets the array count.
  2. Based on the number of items, this builds the informative text string.
  3. Sets the status label text.

Now, you just need to invoke this method when the user changes the table view selection. Add the following code inside the table view delegate extension:

func tableViewSelectionDidChange(_ notification: Notification) {
  updateStatus()
}

When the selection changes this method is called by the table view, and then it updates the status text.

Build and run.

selection label now configured

Try it out for yourself; select one or more files in the table view and watch the informative text change to reflect your selection.

Responding to Double-Click

In macOS, a double-click usually means the user has triggered an action and your program needs to perform it.

For instance, when you’re dealing with files you usually expect the double-clicked file to open in its default application and for a folder, you expect to see its content.

You’re going to implement double-click responses now.

Double-click notifications are not sent via the table view delegate; instead, they’re sent as an action to the table view target. But to receive those notifications in the view controller, you need to set the table view’s target and doubleAction properties.

Note: Target-action is a pattern used by most controls in Cocoa to notify events. If you’re not familiar with this pattern, you can learn about it in the Target-Action section of Apple’s Cocoa Application Competencies for macOS documentation.

Add the following code inside viewDidLoad() of the ViewController:

tableView.target = self
tableView.doubleAction = #selector(tableViewDoubleClick(_:))

This tells the table view that the view controller will become the target for its actions, and then it sets the method that will be called after a double-click.

Add the tableViewDoubleClick(_:) method implementation:

func tableViewDoubleClick(_ sender:AnyObject) {
 
  // 1
  guard tableView.selectedRow >= 0,     
      let item = directoryItems?[tableView.selectedRow] else {
    return
  }
 
  if item.isFolder {
    // 2
    self.representedObject = item.url as Any
  }
  else {
    // 3
    NSWorkspace.shared().open(item.url as URL)
  }
}

Here’s the above code broken out step-by-step:

  1. If the table view selection is empty, it does nothing and returns. Also note that a double-click on an empty area of the table view will result in an tableView.selectedRow value equal to -1.
  2. If it’s a folder, it sets the representedObject property to the item’s URL. Then the table view refreshes to show the contents of that folder.
  3. If the item is a file, it opens it in the default application by calling the NSWorkspace method openURL()

Build and run and check out your handiwork.

Double-click on any file and observe how it opens in the default application. Now choose a folder and watch how the table view refreshes and displays the content of that folder.

Whoa, wait, did you just create a DIY version of Finder? Sure looks that way!

Sorting Data

Everybody loves a good sort, and in this next section you’ll learn how to sort the table view based on the user’s selection.

One of the best features of a table is one- or two-click sorting by a specific column. One click will sort it in ascending order and a second click will sort in descending order.

Implementing this particular UI is easy because NSTableView packs most of the functionality right out of the box.

Sort descriptors are what you’ll use to handle this bit, and they are simply instances of the NSSortDescriptor class that specify the desired attribute and sort order.

After setting up descriptors, this is what happens: clicking on a column header in the table view will inform you, via the delegate, which attribute should be used, and then the user will be able sort the data.

Once you set the sort descriptors, the table view provides all the UI to handle sorting, like clickable headers, arrows and notification of which sort descriptor was selected. However, it’s your responsibility to order the data based on that information, and refresh the table view to reflect the new order.

You’ll learn how to do that right now.

Woot!

Add the following code inside viewDidLoad() to create the sort descriptors:

// 1
let descriptorName = NSSortDescriptor(key: Directory.FileOrder.Name.rawValue, ascending: true)
let descriptorDate = NSSortDescriptor(key: Directory.FileOrder.Date.rawValue, ascending: true)
let descriptorSize = NSSortDescriptor(key: Directory.FileOrder.Size.rawValue, ascending: true)
 
// 2
tableView.tableColumns[0].sortDescriptorPrototype = descriptorName
tableView.tableColumns[1].sortDescriptorPrototype = descriptorDate
tableView.tableColumns[2].sortDescriptorPrototype = descriptorSize

This is what this code does:

  1. Creates a sort descriptor for every column, complete with a key (Name, Date or Size), that indicates the attribute by which the file list can be ordered.
  2. Adds the sort descriptors to each column by setting its sortDescriptorPrototype property.

When the user clicks on any column header, the table view will call the data source method tableView(_:sortDescriptorsDidChange:), at which point the app should sort the data based on the supplied descriptor.

Add the following code to the data source extension:

func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
  // 1
  guard let sortDescriptor = tableView.sortDescriptors.first else {
    return
  }
  if let order = Directory.FileOrder(rawValue: sortDescriptor.key!) {
    // 2
    sortOrder = order
    sortAscending = sortDescriptor.ascending
    reloadFileList()
  }
}

This code does the following:

  1. Retrieves the first sort descriptor that corresponds to the column header clicked by the user.
  2. Assigns the sortOrder and sortAscending properties of the view controller, and then calls reloadFileList(). You set it up earlier to get a sorted array of files and tell the table view to reload the data.

Build and run.

sortable-columns

Click any header to see your table view sort data. Click again in the same header to alternate between ascending and descending order.

You’ve built a nice file viewer using a table view. Congratulations!

Where to Go From Here?

You can download the completed project here.

This macOS NSTableView tutorial covered quite a bit, and you should now feel much more confident in your ability to use table views to organize data. In addition, you also covered:

  • The basics of table view construction, including the unique qualities of headers, rows, columns and cells.
  • How to add columns to display more data.
  • How to identify various components for later reference.
  • How to load data in the table.
  • How to respond to various user interactions.

There is a lot more you can do with table views to build elegant UI for your app. If you’re looking to learn more about it, consider the following resources:

If you have any questions or comments on this tutorial, feel free to join the discussion below in the forums!

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Warren Burton

Warren is an contract iOS and OS X developer living in London. Outside of his day work he creates interesting tools for Xcode development and mind mapping.

When not going screen blind, he goes mountain biking, hiking and surfing.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 15 total!

iOS Team

... 32 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!