I have a post on implementing NSFetchedResultsController
for iOS UITableView
.
This time, let’s take a look at how to implement the same but for the Cocoa framework counterpart - NSTableView
for macOS.
The Differences
The table view APIs are quite different, since the UI representation is pretty different.
- iOS has concept of sections * rows
- macOS has only rows
- iOS concept of 1 cell = 1 row
- macOS’s 1 row = multiple cells, aka columns
In other words, iOS has sections and no columns, while macOS has columns and no sections.
1. Setup NSFetchedResultsController
As usual, create the object as a lazy var.
private lazy var fetchedResultsController: NSFetchedResultsController<Networth> = {
let fetchRequest: NSFetchRequest<Networth> = Networth.fetchRequest()
fetchRequest.predicate = predicate ...
fetchRequest.sortDescriptors = ...
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
In your view controller’s viewDidLoad
, call try fetchedResultsController.performFetch()
.
You might also be interested to read my post on how to use Core Data, including the new container’s view context.
2. Setup table view delegates
NSTableView
has far fewer methods that needs to be implemented. Just 1 for NSTableViewDataSource
and 1 for NSTableViewDelegate
.
// NSTableViewDataSource:
func numberOfRows(in tableView: NSTableView) -> Int {
return fetchedResultsController.fetchedObjects?.count ?? 0
}
// NSTableViewDelegate:
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell: NSTableCellView!
let column = tableView.tableColumns.firstIndex(of: tableColumn!)!
switch column {
case 0:
cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "date"), owner: nil) as? NSTableCellView
case 1:
cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "amount"), owner: nil) as? NSTableCellView
default:
return nil
}
configureCell(cell: cell, row: row, column: column)
return cell
}
private func configureCell(cell: NSTableCellView, row: Int, column: Int) {
let networth = fetchedResultsController.fetchedObjects![row]
switch column {
case 0:
cell.textField?.stringValue = networth.date ?? ""
case 1:
cell.textField?.stringValue = networth.amount ?? ""
default:
break
}
}
configureCell
is an abstraction because we need to use it later. Customize as needed for your app, and for all the columns you have.
3. NSFetchedResultsControllerDelegate
The methods in NSFetchedResultsControllerDelegate
is again a chunk of code that you can simply copy and paste, without any modification:
// NSFetchedResultsControllerDelegate:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?){
switch type {
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath.item], withAnimation: .effectFade)
}
case .delete:
if let indexPath = indexPath {
tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade)
}
case .update:
if let indexPath = indexPath {
let row = indexPath.item
for column in 0..<tableView.numberOfColumns {
if let cell = tableView.view(atColumn: column, row: row, makeIfNecessary: true) as? NSTableCellView {
configureCell(cell: cell, row: row, column: column)
}
}
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade)
tableView.insertRows(at: [newIndexPath.item], withAnimation: .effectFade)
}
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
What’s new is that in .update
, the table view needs to configure for all the columns for that row.
PITFALL: Typical Use
The boilerplate code for NSFetchedResultsControllerDelegate
is given by Apple for a typical use.
However, there are issues. There are bugs.
I have faced a bug where NSTable
render incorrectly, or worse, after calling insertRows
and removeRows
multiple times. For example, the boilerplate code could remove 10 items, and in so calling removeRows
10 times, separately. The table view could crash after eg. removing 5 times, since it is not batched atomically, getting a row could result in out-of-bound exception.
The workaround is to call the method once in controllerDidChangeContent
. How to do that? You need to keep track of the changes in your own instance property.
Or you can simply do a reload()
in controllerDidChangeContent
, which is mentioned in Apple documentation too. A lazy way, but easiest.