Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

NSTableView.clickedRow sometimes is greater than number of rows

Xcode has been downloading many similar crash reports for my app for some time now, related to an index out of range runtime exception when accessing a Swift array. The crashes always happen in methods triggered by user input or during menu item validation when I try to access the data source array by using the following code to determine the indexes of the relevant table rows:

let indexes = clickedRow == -1 || selectedRowIndexes.contains(clickedRow) ? selectedRowIndexes : IndexSet(integer: clickedRow)

I was never able to reproduce the crash until today. When the app crashed in the Xcode debugger, I examined the variables clickedRow and selectedRowIndexes.first, which were 1 and 0 respectively. What's interesting: the table view only contained one row, so clickedRow was effectively invalid. I tried to reproduce the issue several times afterwards, but it never happened again.

What could cause this issue? What are the circumstances where it is invalid? Do I always have to explicitly check if clickedRow is within the data source range?

Do I always have to explicitly check if clickedRow is within the data source range?

I would recommend to.

What is surprising is to have a value of 1 if tour table has only one row. Did it have several rows at any time ?

Could you detail the steps you had to get the crash you reported ?

So, hard to say what the reason is. Could you show more code (such as dataSource and delegate functions of the TableView)

I'm not sure anymore, but it's possible that it had two rows at some point. Then I would have clicked a button that changed the data source array and the table view would have been reloaded with reloadData(). And in the end I clicked another button to add a new item to the data source array and insert the corresponding row in the table view, but since clickedRow was invalid, it crashed.

I have been using NSTableView for different apps for more than 10 years now so I have some experience with it, and the implementation in this case is quite simple. array is the one that can gets swapped whenever category changes, and otherArray is another one that is displayed when no category is selected.

func numberOfRows(in tableView: NSTableView) -> Int {
    return array.isEmpty ? 0 : category.map({ array[$0]?.count ?? 0 }) ?? otherArray.count
}

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
    let cell = MyTableCellView()
    ...
    return cell
}

func tableView(_ tableView: NSTableView, rowActionsForRow row: Int, edge: NSTableView.RowActionEdge) -> [NSTableViewRowAction] {
    ...
}

func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
    ...
}

func tableViewSelectionDidChange(_ notification: Notification) {
    ...
}

OK, I see you are a seasoned developer…

So, best is probably to make code robust by testing the validity of row. Could be a side effect of some values not yet updated when you use them, and it may be hard to find.

It would still be great if an Apple engineer could comment on whether this can indeed happen (and when) or if it's likely a macOS issue. Not only was clickedRow outside of the valid index range, but it was also not -1, and I thought that it would only be set for the duration of table view action methods (e.g. context menu item actions, double click action), not during methods called by buttons outside of the table view.

NSTableView.clickedRow sometimes is greater than number of rows
 
 
Q