I’m a real beginner when it comes to SwiftUI. I’ve dabbled here and there, but for the most part, I really don’t know what I’m doing. Recently, an opportunity came up to give it another try. For this particular experiment, I wanted to wrap up a pre-existing view so I could access it from SwiftUI. That part is pretty standard stuff. However, I ran into an interesting issue, and I bet I’m not alone.

The Problem

When I’m learning a new API, I like to start with a new project. This helps me focus on the task and avoid the complexities of integration with a larger project. My goal was to wrap up an NSTableView and get that into a SwiftUI view hierarchy. So, I created my new project and proceeded to follow along with Apple’s SwiftUI tutorial on Interfacing with UIKit, adapting it to AppKit and my problem as needed.

I was feeling pretty good because I got something working quite quickly. This implementation follows the View-NSView-Coordinator pattern, and the structure makes sense once you stare at it for a bit.

Here’s the code I came up with:

struct EventsTableView: NSViewRepresentable {
    var events: [String]

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

    func makeNSView(context: Context) -> NSScrollView {
        let column = NSTableColumn(identifier: .exampleColumn)
        column.width = 100.0

        let tableView = NSTableView()

        tableView.headerView = nil
        tableView.addTableColumn(column)
        tableView.delegate = context.coordinator
        tableView.dataSource = context.coordinator

        let view = NSScrollView()

        view.documentView = tableView

        return view
    }

    func updateNSView(_ nsView: NSScrollView, context: Context) {
        let tableView = nsView.documentView as! NSTableView

        tableView.reloadData()
    }
}

extension EventsTableView {
    final class Coordinator: NSObject, NSTableViewDataSource, NSTableViewDelegate {
        var parent: EventsTableView

        init(_ parent: EventsTableView) {
            self.parent = parent
        }

        func numberOfRows(in tableView: NSTableView) -> Int {
            return parent.events.count
        }

        func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
            let reusedView = tableView.makeView(withIdentifier: .exampleColumn, owner: self)
            let view = reusedView as? NSTextField ?? NSTextField(labelWithString: "")

            view.stringValue = parent.events[row]

            return view
        }
    }
}

I said that things were working. But, once I tried a little more experimenting, I quickly discovered that the contents of my table view would never change, even though I was reloading the table on updates.

Of course, in hindsight the problem now seems obvious. And, you might very well have already noticed it. But, if you haven’t, don’t feel bad as it took me quite a bit of debugging and thinking to fully understand what was wrong.

Parent?

SwiftUI interoperability is all based around a wrapper view, EventsTableView and its Coordinator. That Coodinator instance will then get passed back as context when the view needs to change. This relationship between wrapping view and coordinator is critical, as is understanding their lifecycle relationship.

See, a SwiftUI view can (and definitely does) get re-created very liberally during normal execution. But, your coordinator instance is created only once, and then is re-used as much as possible. This is pretty standard value type/reference type stuff. When a Coordinator takes in the wrapping view as an initialization parameter, it makes a copy of that first view. That copy is not updated when the SwiftUI wrapper view is re-created.

My issue was the parent property (and its associated events) were copied once when the Coordinator was created, and then never updated. Once I realized this, I felt dumb. Views are value types, of course it wasn’t updating! But, I don’t want to be too hard on myself, because this pattern of a Coordinator capturing its parent view is ubiquitous. It is present in many, many, many blog posts on the subject. And, all of them, including Apple’s (ostensibly authoritative) tutorial, use this parent pattern. I assume that’s where this all started.

Capture Properties

In my case, the solution was straight-forward. The key was to capture and update the data in the coordinator instance.

struct EventsTableView: NSViewRepresentable {
    var events: [String]

    func makeCoordinator() -> Coordinator {
        Coordinator(events: events) // <- Here!
    }

// snip...

    func updateNSView(_ nsView: NSScrollView, context: Context) {
        context.coordinator.events = events // <- And here!

        let tableView = nsView.documentView as! NSTableView

        tableView.reloadData()
    }
}

Once I fully realized that the coordinator instance is long-lived, capturing the parent made no sense. But, it did trick me into thinking I had a reference-type relationship, which would have worked. If only this property was called firstParent or creatingView, perhaps I wouldn’t have made this mistake.

Never Capture Parent

After going through all of this, I’m not sure I can think of a reason why you’d ever need to capture a copy of a Coordinator’s creating view. If you know of a reason why this parent-Coordinator pattern might be useful, please let me know. But, even if there are some appropriate situations, I still think that it’s so easy to get confused, just like I did, it should probably be avoided.