Adding an NSTableView to a SwiftUI View
Tags: swiftui, appkit, swift, macos
Published: 2020-03-18 22:38:52

---
title: Adding an NSTableView to a SwiftUI View
published: true
description: AppKit <-> SwiftUI interactions are poorly documented. NSTableView is useful. What do?
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/zfnw0438ply19ee98mn6.jpeg
tags: #swiftui #appkit #swift #macos
---

SwiftUI made a [surprisingly good first impression](https://dev.to/hugh_jeremy/swiftui-first-impressions-pain-analysis-5d6i). Of course, things get more complicated as soon as you want to build something actually useful. Tables are a fundamental part of many macOS applications. How can we build them in SwiftUI?

Apple pushes the SwiftUI [`List`](https://developer.apple.com/documentation/swiftui/list) pretty hard. It's great for iPhone GUIs. It is woefully inadequate for the desktop. Compared to the old AppKit `NSTableView` it is a children's toy.

You can certainly build tables out of `List`, using rows of `HStack` views. My test case was the Draft Rugby [Player Stats table](https://draftrugby.com/player-stats). Using [draft-sport-swift](https://github.com/hwjeremy/draft-sport-swift) to source the data, `List` gave me something approaching serviceability:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pb1c6djs4xvttb0cvz9g.png)

In code, it's not pretty. Lots of manual column-width calculation. Manual row banding. Manual row formulation in a dedicated row view. In the end, it involved writing lots of code to get something that _approximated_ the aesthetic of an `NSTableView`, but without all the functionality that `NSTableView` includes.

Creating an `NSTableView` also involves writing lots of code. However, you get a lot of bang for your buck. Ideally, we could slap an `NSTableView` straight into our SwiftUI app. Fortunately, we can!

### Step 1: Create `NSViewController` / `NSTableView` as normal

```swift
import Foundation
import AppKit

class PlayerNSTableView: NSTableView {
    // Define your NSTableView subclass as you would in an AppKit app
}

class class PlayerNSTableController: NSViewController {
    // Define your NSViewController subclass as you would in an AppKit app
}
```

### Step 2: Create an `NSViewControllerRepresentable` wrapper

SwiftUI is all about `View` implementations. AppKit is all about `NSViewController`, `NSView`, the relationships between them, and a bunch of other crap. `NSViewControllerRepresentable` is the bridge between the AppKit and SwiftUI worlds. It is itself a `View` conforming protocol, and can be passed around in SwiftUI like any other `View` struct.

Create a struct conforming to `NSViewControllerRepresentable`, and add the required protocol stubs. Observe the tangled mess that results:

```swift
import Foundation
import AppKit
import SwiftUI
import DraftSport

struct PlayerNSTable: NSViewControllerRepresentable {
    
    @Binding var players: Array<Player>?

    typealias NSViewControllerType = PlayerNSTableController
    
    func makeNSViewController(
        context: NSViewControllerRepresentableContext<PlayerNSTable>
    ) -> PlayerNSTableController {
        return PlayerNSTableController()
    }
    
    func updateNSViewController(
        _ nsViewController: PlayerNSTableController,
        context: NSViewControllerRepresentableContext<PlayerNSTable>
    ) {
        
        if let players = players {
            nsViewController.refresh(players)
        }
    
        return

    }

}
```

There's a lot to unpack here. Never fear, the only bit you really need to care about is the body of `makeNSViewController()`. Inside the body, you will see `return PlayerNSTableController()`. That's the key: Initialise the `NSViewController` subclass you defined in Step 1.

### Step 3: Manipulate the wrapped `NSViewController` from SwiftUI

We're obviously going to want to manipulate the AppKit table data from SwiftUI. In the above `NSViewControllerRepresentable` implementation, you will notice the line `@Binding var players: Array<Player>?`. This is a bridge passing data between the SwiftUI and AppKit worlds.

Like any other struct conforming to `View`, `PlayerNSTable` may observe changes to its properties and update the GUI in response.  The magic sauce is the `@Binding` decorator. Now, when `.players` is changed, the `.updateNSViewController()` method of our `NSViewControllerRepresentable` is called.

Inside `.updateNSViewController()` we can do whatever we like. I happen to expose a `.refresh(:Array<Player>)` method on my `PlayerNSTableController`, which ultimately calls `.reloadData()` on the underlying `NSTableView`. It's up to you how you want to feed new data into your `NSViewController` / `NSTableView` pair.

### Step 4: Add your wrapped AppKit view to a SwiftUI view

You can now add your `NSViewControllerRepresentable` implementation to any SwiftUI `View`. Here's my implementation, `PlayerNSTable`, being added to an enclosing `View`:

```swift
import SwiftUI
import DraftSport

struct PlayerTable: View {

    @State var players: Array<Player>? = nil
    
    var body: some View {
        PlayerNSTable(
            players: self.$players
        )
            .frame(alignment: .topLeading)
            .onAppear(perform: retrievePlayers)
    }
    
    func retrievePlayers() -> Void {
        self.players = nil
        Player.retrieveMany(
            season: Season(publicId: "2020")
        ) { (error, players) in
            guard let players = players else {
                fatalError("No players")
            }
            self.players = players
        }
    }
}
```

Note that when `PlayerNSTable` appears, `Player` data are retrieved via the `retrievePlayers()` method. As with any other SwiftUI `View`, changes to `@State` decorated properties are observed. When `Player.retrieveMany()` executes its callback closure, the `.players` property is updated. This update ultimately triggers the `updateNSViewController()` method we discussed in Step 2.

The end result:

![](https://dev-to-uploads.s3.amazonaws.com/i/ugtzcpr41gxwlc418p1u.png)

A fully functional `NSTableView` inside a SwiftUI `View` hierarchy. Phew! That was intense. If you have any questions, find me [@hugh_jeremy](https://twitter.com/hugh_jeremy) on Twitter.