Today, let's talk about table view in AppKit for OS X. Programming for OS X have a long history behind. History that is not always known to the iOS programmers, however it is the Mother of UIKit.
An NSTableView object displays data for a set of related records, with rows representing individual records and columns representing the attributes of those records.
NSTableView is a lightweight view component to display data in a table, with columns and rows, although it have the big brother:
NSOutlineView is a subclass of NSTableView that uses a row-and-column format to display hierarchical data that can be expanded and collapsed, such as directories and files in a file system.
The problem with NSOutlineView is that it's really huge and need much work to setup. It's the Swiss Army Knife of table views, but is seems like overkill for a simple tables. On the other hand, simple table view is too simple. NSTableView allows display columns of rows. That's it.
If you are familiar with UIKit framework, you must know UITableView which seem to be equivalent of NSTableView. But it's not.
Sections
One feature I need, and found missing, is lack of Sections. NSTableView don't support, in any easy way, displaying data organised in sections. What is section?
Section is the group of row cells presented in the same column, with header (or without) for example:
this is the section "PEOPLE" with items "Frank", "Monica", "Natalie", "Alice".
Below I'll discuss one of the possibility how to add sections to plain NSTableView. Turn out it's not that hard but require some work to do (obviously)
Data source
At start, I created new protocol, extendeding default data source protocol to support sections, with three functions required to calculate sections.
I named it NSTableViewSectionDataSource
:
protocol NSTableViewSectionDataSource: NSTableViewDataSource {
func numberOfSectionsInTableView(tableView: NSTableView) -> Int
func tableView(tableView: NSTableView, numberOfRowsInSection section: Int) -> Int
func tableView(tableView: NSTableView, sectionForRow row: Int) -> (section: Int, row: Int)
}
SectionForRow
Implementation of tableView(tableView: NSTableView, sectionForRow row: Int)
is responsible for translation flat row representation into group based structure (sections with rows).
func tableView(tableView: NSTableView, sectionForRow row: Int) -> (section: Int, row: Int) { ... }
is responsible for translation flat row representation into structured group based structure (sections). For given input parameter of row
index, it returns index of section along index of row inside this section.
For given 4 sections, plain table row number 5 is the first row of section 3, with index values (section: 2, row: 0)
Hard part
The hard part is make calculations you don't want to make. Ok, I made it for you. My logic is implemented by function sectionForRow(row,counts)
where row is a row index, and counts is an array of count of items per section, and provide number of section:
private func sectionForRow(row: Int, counts: [Int]) -> (section: Int?, row: Int?) {
let total = reduce(counts, 0, +)
var c = counts[0]
for section in 0..<counts.count {
if (section > 0) {
c = c + counts[section]
}
if (row >= c - counts[section]) && row < c {
return (section: section, row: row - (c - counts[section]))
}
}
return (section: nil, row: nil)
}
where counts
is the array of counts of items for available sections. Typical use:
let (section, sectionRow) = sectionForRow(row, counts: [2,3,5])
I'll use this function to implement rest of the functions from protocols: NSTableViewDelegate
, NSTableViewDataSource
and NSTableViewSectionDataSource
.
the most important function of NSTableViewSectionDataSource
protocol to implement, and to use everywhere is tableView(tableView: NSTableView, sectionForRow row: Int) -> (section: Int, row: Int)
. This is my implementation:
func tableView(tableView: NSTableView, sectionForRow row: Int) -> (section: Int, row: Int) {
if let dataSource = tableView.dataSource() as? NSTableViewSectionDataSource {
let numberOfSections = dataSource.numberOfSectionsInTableView(tableView)
var counts = [Int](count: numberOfSections, repeatedValue: 0)
for section in 0..<numberOfSections {
counts[section] = dataSource.tableView(tableView, numberOfRowsInSection: section)
}
let result = self.sectionForRow(row, counts: counts)
return (section: result.section ?? 0, row: result.row ?? 0)
}
assertionFailure("Invalid datasource")
return (section: 0, row: 0)
}
so as you can see, it will return section and row for my table view based on the configuration of my table view. Yay!
Configuration
Configuration is made by a NSTableViewSectionDataSource
protocol. It is based on UITableDataSource protocol known from UIKit. There is delegate function for number of sections, and for items for given section.
Views
Views are handled by delegate (NSTableViewDelegate
) of my NSTableView instance. This is how it can be easily handled now:
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cellView = tableView.makeViewWithIdentifier("CellView", owner: self) as! NSTableCellView
if let dataSource = tableView.dataSource() as? NSTableViewSectionDataSource {
let (section, sectionRow) = dataSource.tableView(tableView, sectionForRow: row)
// here! build view for given section and row index
}
return cellView
}
Of course in this example I miss header of section itself. It's not that hard to implement though. I have to:
- adjust total number of rows with +1 per section
- return special header view, for rows at index 0, for every section
That's it. Now I can build fully featured, organised in sections, table view, and I did: ViewController.swift
Result
Here is the result
which looks like a list but in fact it's combination of two sections.
Fully working example project can be found on my Github account: krzyzanowskim/NSTableView-Sections. Give a star if you found it helpful.
PS. Cover photo by https://www.flickr.com/photos/42931449@N07/5418394438