Table views are one of the most ubiquitous controls in OS X 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 OS X 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
Click here to 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 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 to go looking:
- 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.
Next, you need to add some constraints. Click the Pin button in the Auto Layout toolbar. In the popup that appears, set the constraints as follows:
- Top, Leading and Trailing: 0.
- Bottom: 22, making sure to select View in the constraint’s option menu.
Click the triangular Resolve Auto Layout Issues button in the Auto Layout toolbar, and under the Selected Views section click on Update Frames.
You’ve just preempted any Auto Layout warnings in the canvas by updating the frames of the views to match their 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 OS X. 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, and now that you get 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 has now 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.
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
Note: There are two ways that you can populate a tableview—either using the datasource and delegate protocols you’ll see in this OS X NSTableView tutorial, or via Cocoa bindings. When you start a project ensure that you choose between the two approaches. We’ll have tutorials on the site in the future that cover using Cocoa Bindings
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
numberOfRowsInTableView(_:)
that returns the number of rows the table will display. - The table view calls the delegate method
tableView(_: viewForTableColumn:, 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 the assistant editor and Control-drag from the table view onto 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.
Implement the required data source method in the ViewController
by adding this code at the end of ViewController.swift:
extension ViewController : NSTableViewDataSource { func numberOfRowsInTableView(tableView: NSTableView) -> Int { return directoryItems?.count ?? 0 } } |
This creates an extension that conforms to the NSTableViewDataSource
protocol and implements the required method numberOfRowsInTableView(_:)
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 { func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? { var image:NSImage? var text:String = "" var cellIdentifier: String = "" // 1 guard let item = directoryItems?[row] else { return nil } // 2 if tableColumn == tableView.tableColumns[0] { image = item.icon text = item.name cellIdentifier = "NameCellID" } else if tableColumn == tableView.tableColumns[1] { text = item.date.description cellIdentifier = "DateCellID" } else if tableColumn == tableView.tableColumns[2] { text = item.isFolder ? "--" : sizeFormatter.stringFromByteCount(item.size) cellIdentifier = "SizeCellID" } // 3 if let cell = tableView.makeViewWithIdentifier(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(_:viewForTableColumn: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
makeViewWithIdentifier(_: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 the viewDidLoad()
method:
tableView.setDelegate(self) tableView.setDataSource(self) |
Here you tell the table view that its data source will be the view controller and that setDelegate(_:)
and setDataSource(_:)
will call its methods.
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 File… or the Command+O keyboard shortcut and watch magic happen! Now the table is full of contents from the folder you just selected.
Nice job!
But nothing in life is ever that easy, right? Look carefully, because there is a peculiar thing going on with the cells. All the text is truncated, even when there is ample horizontal space.
That’s because view cells created by Interface Builder have no Auto Layout constraints by default, but you need them to make the cells adapt to the column width.
Adding Constraints to View Cells
Open Main.storyboard. Select the image view located inside the cell view for the Name column, and click on the Pin button to add the following three constraints:
- Leading: 3
- Height: 17
- Width: 17
Keep the image selected and click the Align button in the Auto Layout toolbar. Add a Vertically in Container alignment constraint.
Select the text field in this cell view, which is called Table View Cell and is an NSTextField Class type.
Add the following constraints:
- Leading: 7
- Trailing: 3
- Vertically in Container alignment constraint
Repeat this process for the text fields in the Modification Date and Size cells, adding the following constraints to these text fields:
- Leading: 3
- Trailing: 3
- Vertically in Container alignment constraint.
Build, run and then select a folder.
Now you can resize the columns and see that the text is not truncated where there is ample space. Awesome!
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 then the selection changes in the table view, you need to implement the tableViewSelectionDidChange(_:)
method 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 = "" } 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: NSNotification) { updateStatus() } |
When the selection changes this method is called by the table view, and then it updates the status text.
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 OS X, 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 OS X documentation.
Add the following code inside the viewDidLoad()
method of the ViewController
:
tableView.target = self tableView.doubleAction = "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 } else { // 3 NSWorkspace.sharedWorkspace().openURL(item.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, 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 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 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, e.g., Name, Date, 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:oldDescriptors:)
, 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() } } 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 OS X 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 constraints and truncation are related
- 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 2011 – Session 120 Video View Based NSTableView Basic to Advanced
- TableViewPlayGround sample code that contains source 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! Thanks for joining me again!
The post OS X NSTableView Tutorial appeared first on Ray Wenderlich.