How to use NSTableView in SwiftUI

Author

Wasim Lorgat

Published

January 25, 2023

I recently started learning macOS development using SwiftUI as part of my latest project: building a macOS Jupyter frontend. While I’m loving Swift (the language) and SwiftUI (the UI framework), it’s sometimes extremely difficult to find out information that feels like it should be readily available.

The latest such case is how to use an NSTableView in SwiftUI. SwiftUI’s new List is great for iOS and multiplatform apps, but doesn’t seem to be designed for desktop-specific apps which can do with much more information-dense UIs.

SwiftUI has the newer Table too, but it’s also quite limited at this stage. For example, I don’t think it’s possible to make an entire row clickable.

This left me wanting to try out the much more battle-tested NSTableView – but as an Apple dev noob, I couldn’t get a minimal example up and running after a few hours of tinkering!

… So here’s a snippet you can copy paste.1 Keep reading below if you’d like to see how it works step-by-step.

import SwiftUI

struct TableView: NSViewRepresentable {
    class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
        let data = ["Apple", "Banana", "Cherry"]

        func numberOfRows(in tableView: NSTableView) -> Int {
            data.count
        }

        func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
            NSTextField(labelWithString: data[row])
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    func makeNSView(context: Context) -> NSTableView {
        let tableView = NSTableView()
        tableView.delegate = context.coordinator
        tableView.dataSource = context.coordinator
        tableView.addTableColumn(NSTableColumn())
        return tableView
    }

    func updateNSView(_ nsView: NSTableView, context: Context) {
        // Do nothing
    }
}

Step-by-step:

Create your table view struct, conforming to NSViewRepresentable. This is a standard way of using AppKit/UIKit views in your SwiftUI applications.

In makeNSView, create the NSTableView with a single column, and leave updateNSView blank for now:

struct TableView: NSViewRepresentable {
    func makeNSView(context: Context) -> NSTableView {
        let tableView = NSTableView()
        tableView.addColumn(NSTableColumn())
        return tableView
    }

    func updateNSView(_ nsView: NSTableView, context: Context) {
        // Do nothing
    }
}

Create a Coordinator, subclassing:

Implement makeCoordinator, returning an instance of Coordinator, then link it to the table view in makeNSView:

struct TableView: NSViewRepresentable {
    class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    func makeNSView(context: Context) -> NSTableView {
        let tableView = NSTableView()
        tableView.delegate = context.coordinator
        tableView.dataSource = context.coordinator
        tableView.addColumn(NSTableColumn())
        return tableView
    }

    func updateNSView(_ nsView: NSTableView, context: Context) {
        // Do nothing
    }
}

You still won’t see anything being rendered yet, since we still need to implement NSTableViewDelegate and NSTableViewDataSource methods.

For this minimal example, we’ll use a simple static array of strings defined right in the coordinator, although in practice you would probably get data from the view.

Implement NSTableViewDataSource’s numberOfRows and NSTableViewDelegate’s tableView(tableView:viewFor:row). The former returns the length of our array. The latter returns an NSTextField created from the corresponding row of data.

    class Coordinator: NSObject, NSTableViewDelegate, NSTableViewDataSource {
        let data = ["Apple", "Banana", "Cherry"]

        func numberOfRows(in tableView: NSTableView) -> Int {
            data.count
        }

        func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
            NSTextField(labelWithString: data[row])
        }
    }

That’s it! This is the minimal implementation of an NSTableView in SwiftUI that I could find. Let me know on Twitter, via email, or via the GitHub discussion below if you have any comments or suggestions.

Here are some next steps I have in mind:

Let me know if you’d find these helpful!

Footnotes

  1. Many thanks to Alex Grebenyuk whose article and repo I heavily referenced to figure this out.↩︎