Selection disappearing in NSTableView inside SwiftUI

I have a SwiftUI view which contains an NSTableView wrapped with an NSViewControllerRepresentable struct. This has a Coordinator, which I set up as the NSTableView's delegate. There is an implementation of tableViewSelectionDidChange to pass the selected row index up to the enclosing view.

The problem is that I can click and select a row, but the selection disappears, usually on the first click, and always when clicking a column header to sort. I have looked at the various delegate methods for notifying about selection changing, and they don't seem to be called.

How can I find where the selection is being changed?

Accepted Reply

I never got an answer, but I solved my problem by redesigning the table to be expressible as a SwiftUI List, which does away with the awkward interaction with NSTableView. I got a cleaner design in some ways, but I lost a couple of things, particularly the ability to shade alternate rows to make it easier to read, and sorting by columns. Neither is a show-stopper, so I'm going with what I've come up with.

Replies

Since there's been no reply, here's most of the code for the NSViewControllerRepresentable class:
Code Block Swift
struct LayoutsTableView: NSViewControllerRepresentable {
typealias NSViewControllerType = LayoutsTableController
@Binding var layoutsList: [LayoutData]
    @Binding var selectedIndex: Int
func makeNSViewController(context: Context) -> LayoutsTableController {
let theTable = LayoutsTableController(nibName: "LayoutsTable", bundle: Bundle.main)
theTable.layoutList = layoutsList
        theTable.tableDelegate = context.coordinator // So that the table can set its delegate after it appears
        context.coordinator.tableController = theTable
return theTable
}
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
func updateNSViewController(_ nsViewController: LayoutsTableController, context: Context) {
nsViewController.layoutsTable.reloadData()
}
    final class Coordinator: NSObject, NSTableViewDelegate {
        var parentView: LayoutsTableView
        var tableController: LayoutsTableController?
        init(_ parent: LayoutsTableView) {
            parentView = parent
        }
        // MARK: Table Delegate methods
        func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
// returns the appropriate view
        }
        func tableViewSelectionDidChange(_ notification: Notification) {
            let selectedRow = tableController?.layoutsTable.selectedRow ?? -1
            parentView.selectedIndex = selectedRow
        }
    }
}


Any clues, anyone?
I'm having the same issue. Have you had any luck solving it?
I'm currently using a workaround:
Code Block
extension NSTableView {
    func reloadDataSavingSelections() {
        let selectedRows = selectedRowIndexes
        reloadData()
        selectRowIndexes(selectedRows, byExtendingSelection: false)
    }
}

So I get a SwiftUI warning (Modifying state during view update, this will cause undefined behavior.) in exchange for the desired behavior. Which, for obvious reasons, is annoying and dangerous in the long term.
The additional step of saving the select rows in the class and comparing the old selection with the new selection would probably even fix the SwiftUI warning but feels even more wrong...

I never got an answer, but I solved my problem by redesigning the table to be expressible as a SwiftUI List, which does away with the awkward interaction with NSTableView. I got a cleaner design in some ways, but I lost a couple of things, particularly the ability to shade alternate rows to make it easier to read, and sorting by columns. Neither is a show-stopper, so I'm going with what I've come up with.

I ran into this too, but managed to find a solution.

The updateNSView method. was getting called very frequently and so reloading the table which lost the section. I made this function check if its data had changed before reloading and that worked.

I had passed the data to the Coordinator for use in the data source & delegate, so this code worked for me:

  func updateNSView(_ nsView: NSTableView, context: Context) {
    if tableData != context.coordinator.tableData {
      context.coordinator.tableData = tableData
      nsView.reloadData()
    }
  }

The elements in my data array were already Equatable, so this works fine.