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.
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:
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).
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.
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.
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.
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:
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
orNSTableCellView
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.
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.
Rename the column headers so the text is more descriptive. Select the first column in the View Controller Scene.
Open the Attributes Inspector and change the column Title to Name.
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. :]
Build and run. Here’s what you should see:
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.
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.
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.
Repeat the process for the cell views in the second and third columns, naming the identifiers DateCellID and SizeCellID respectively.
Populating the Table View
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.
The visualization process is a collaboration between the table view, delegate and data source:
- The table view calls the data source method
numberOfRows(in:)
that returns the number of rows the table will display. - 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.
Make sure that the Type is NSTableView and the Connection is Outlet. Name the outlet tableView
.
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:
- If there is no data to display, it returns no cells.
- Based on the column where the cell will display (Name, Date or Size), it sets the cell identifier, text and image.
- 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.
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.
- 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. - Based on the number of items, this builds the informative text string.
- 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.
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:
- 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. - 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. - If the item is a file, it opens it in the default application by calling the
NSWorkspace
methodopenURL()
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.
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:
- 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.
- 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:
- Retrieves the first sort descriptor that corresponds to the column header clicked by the user.
- Assigns the
sortOrder
andsortAscending
properties of the view controller, and then callsreloadFileList()
. You set it up earlier to get a sorted array of files and tell the table view to reload the data.
Build and run.
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:
- Apple’s Table View Programming Guide for Mac.
- WWDC 2016 – Session 239 Video – Crafting Modern Cocoa Apps for a fast course in how to build a cutting edge table view.
- WWDC 2011 – Session 120 Video View Based NSTableView Basic to Advanced.
- TableViewPlayGround has Objective-C sample code to show how to create different kinds of custom table views.
If you have any questions or comments on this tutorial, feel free to join the discussion below in the forums!
The post macOS NSTableView Tutorial appeared first on Ray Wenderlich.