Getting Started with Cocoa Bindings for Populating an NSTableView

6 minute read

A gentle introduction to NSTableView.

Let’s say you have some data:

let menu = [Food(id: 1, name: "Banana Split", price: 12), Food(id: 2, name: "Fried Banana", price: 7)]

If you want to learn to build a simple native Mac app with Storyboards (No SwiftUI today, sorry! If only Apple’s OS betas were less scary, because right now nobody should install them) in Xcode that displays the data with an NSTableView, this post is for you.

Let’s begin!

How?

Here I’ll assume no knowledge of Xcode or macOS development, because this is the post I wish I had when I started my first Cocoa project.

If you haven’t already, make a new project like so: File > New > Project… > macOS > Cocoa App.

The Data Model

We need a way to represent food items that can be key-value compliant for Cocoa, which here means that our data needs to be accessible from the Objective-C runtime. Make a file in the root of the project called Food.swift with the following contents:

class Food: NSObject {
    @objc dynamic var id: Int
    @objc dynamic var name: String
    @objc dynamic var price: Float
    init(id: Int, name: String, price: Float) {
        self.id = id
        self.name = name
        self.price = price
    }
}

Take note of the @objc dynamic annotations and the fact that Food inherits from NSObject. Other than that, it’s just a class with a constructor.

File Creation Instructions

Do File > New > File… (or ⌘-N) and select “Swift File” when prompted. Click “Next” and give the new file a name, while making sure its destination is the root of the project (next to the AppDelegate.swift file).

Hello, Cocoa!

Make another file called MainViewController.swift that contains the following boilerplate:

import Cocoa
class ViewController: NSViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // TODO: populate the table view
    }
}

We need to make a menuItems array to bind to the table view, so add the following declaration below the opening class bracket: @objc dynamic var menuItems: [Food] = []

The Layout

Open “Main.storyboard” via the Project Navigator panel.

Show the object library with ⌘-Shift-L and search for “Table View” in the window that pops up. Drag the entry of the same name onto the NSView in Interface Builder, which is below the representation of the Window Controller.

Autoresizing!

While you’re at it, make sure the right-most panel is shown by making sure the image button with the tooltip “Hide or show the inspectors” is outlined with blue. I’ll call this the “inspector panel” from now on. Here’s a cheat sheet I threw together:

inspector panel cheatsheet

Now you can click on the Size Inspector, which looks like a ruler and is the fourth icon from the right on the inspector panel, scroll down, and in the “Autoresizing” section, click all the light red arrows to make the table view expand along with the window.

Your window should look something like this:

All the red resizing arrows are highlighted and the table view's scroll view parent is selected in the Document Outline.

Columns

To display the name and price of each food item, we will need two table view columns. Expand the “Table View” in the Document Outline, and if there aren’t already two “Table Column” objects in the view, duplicate the current one with ⌘-D.

Go ahead and double-click on each column header and type the names “Name” and “Price”, respectively.

Outlets and Bindings

Now we need to do the following:

  1. Add an Array Controller.
  2. Bind the Array Controller to the menu list.
  3. Add an IBOutlet so the table view can be referenced from ViewController.
  4. Bind the table cell views to the Food attributes that correspond to each column.
  5. Bind the table view to the array controller.

Add an Array Controller

Click on the “View Controller” group above the layout panel to reveal a new list of icons, and drop “NSArrayController” from the Object Library next to the other top-level objects:

The Array Controller is highlighted in the Object Library and is ready to drop onto the storyboard, which is open in the main panel.

The Array Controller is now next to the other icons directly above the view.

Bind the Array Controller to the data array defined in MainViewController.swift

Make sure the new Array Controller is selected in the top-level object group and open up the Bindings inspector pane. Under Controller Content > Controller Array, make sure the “Bind To” checkbox is checked, change the corresponding controller in the dropdown to “View Controller”, and write menuItems for the “model key path”. This will be the name of the list our UI auto-updates from.

Add an IBOutlet so the table view can be referenced from ViewController

Make sure the “Table View” item is selected in the Document Outline, right click, and drag the “New Referencing Outlet” circle to create a line segment that ends at the top of the class definition:

A line connects the Table View context menu with the target location of the outlet, which is in the class definition.

Now make a name for the outlet, like tableView, change “Weak” to “Strong” (because the table view and view controller must both exist at the same time), and click “Connect”.

The ViewController class should now look like this:

class ViewController: NSViewController {
    @objc dynamic var menuItems: [Food] = []
    @IBOutlet var tableView: NSTableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        // TODO: populate the table view
    }
}

Bind the table cell views to the instance variable names that correspond to each column

Now let’s make the app actually display what it is supposed to. For both columns, select the NSTextFieldCell “Table View Cell” (which is below the NSTextField “Table View Cell”, which is below the NSTableCellView “Table Cell View”, which is below each NSTableColumn; that’s a mouthful!), and in the Bindings inspector pane (the second icon from the right in the inspector panel), check “Bind To”, make sure the target is “Table Cell View”, and set both “Model Key Path”s to “objectValue.name” and “objectValue.price”, respectively. See the following annotated screenshot for a recap of what you should do before moving on to the next section:

Each innermost Table View Cell's model key path is the path to the attribute inside the Food object (objectValue.name or objectValue.price) the row represents. Additionally, the Table View Cell should be bound to the Table Cell View above it in the object hierarchy.

Bind the table view to the array controller

Before we can take advantage of the connection between the array controller and the code, we need to connect the table view to the array controller. Open up the Bindings pane for the table view and under “Table Content” expand the “Content” group. Check “Bind To” and make sure the corresponding dropdown’s value is “Array Controller”.

Remember, we can now refer to our NSTableView as tableView from the code-behind (this will help when you want to implement actions on the table view and do things in response to changes of its state) and load Food to menuItems, which will automatically trigger an update of the contents of the table view, thanks to all the binding work you just did (you did follow along in Xcode, right?)!

Data Loading!

Now that we’ve come this far we can pretend like we’re loading from a persistent data store:

class ViewController: NSViewController {
    @objc dynamic var menuItems: [Food] = []
    @IBOutlet var tableView: NSTableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var menu: [Food] = []
        // TODO: read this data from a flat file or database
        menu = [Food(id: 1, name: "Banana Split", price: 12), Food(id: 2, name: "Fried Banana", price: 7)]
        // populate the table view
        menuItems = menu
    }
}

Bonus points for using Grand Central Dispatch (this is getting out of hand) to “load” the menu.

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var menu: [Food] = []
        DispatchQueue.global(qos: .userInteractive).async {
            // TODO: read this data from a flat file or database
            menu = [Food(id: 1, name: "Banana Split", price: 12), Food(id: 2, name: "Fried Banana", price: 7)]
        }
        // populate the table view with the loaded items on the main thread
        DispatchQueue.main.async {
            self.menuItems = menu
        }
    }

That’s it!

Compatibility

This guide was written for Xcode Version 11.0 beta (11M336w) because I like shiny new tools, but it should work with Xcode 10 or whatever the stable version is when you’re reading this. I guess that’s one advantage you get when you stick with old and boring things like Cocoa.

🍫

Cocoa on!

Solomon