NSTableView Variable Row Heights Broken on macOS Ventura 13.0

Variable row heights in your NSTableView might be broken in your apps on macOS Ventura 13.0 – it’s fixed with the upcoming 13.1, but that’s only available as a beta at the moment.

When you replace table contents by calling aTableView.reloadData(), this will ingest the new data as usual, but the old row heights won’t be forgotten. This can affect scrolling. The row height cache, it seems, isn’t properly invalidated or cleared.

NSTableView and NSOutlineView now automatically estimate row heights for view-based table views whose delegates implement tableView(_:heightOfRow:) and provide variable row heights. This provides performance improvements for table views with large numbers of rows by reducing the frequency of the calls to tableView(_:heightOfRow:).

(macOS 13 Release Notes)

Kuba Suder summarized this:

In macOS Ventura NSTableView now calculates the row heights lazily:

  • row heights are calculated for rows which are in or near the scrolling viewport
  • for the rest of the rows, NSTableView estimates the height based on the row heights that it has already measured
  • as the table is scrolled, the table view requests more row heights as needed and replaces the estimates with real measurements

@SkipRousseau shared how to fix this in a Slack:

  1. Your app can opt out of row height estimation.

    Test this via launch arguments in Xcode by adding -NSTableViewCanEstimateRowHeights NO. That essentially overrides a UserDefaults key.1

    To do this programmatically, call UserDefaults.standard.set(false, forKey: "NSTableViewCanEstimateRowHeights") early in the app’s launch routine, e.g. during applicationWillFinishLaunching.

  2. Invalidate the cache for each row index, in code via NSTableView.noteHeightOfRows(withIndexesChanged:). Skip’s sample to invalidate the cache for all rows is:

     // Right after reloadData,
     NSAnimationContext.beginGrouping()
     NSAnimationContext.current.duration = 0
     let entireTableView: IndexSet = .init(0 ..< self.tableView.numberOfRows)
     self.tableView.noteHeightOfRows(withIndexesChanged: entireTableView)
     NSAnimationContext.endGrouping()
    

For now, I tend towards disabling NSTableViewCanEstimateRowHeights by default to get the old behavior and to prevent the bug on Ventura, and optionally enable this for macOS 13.1 during the launch procedure. Opting-out of the new behavior sounds easier to maintain than invalidating row indexes on 13.0 in code.

  1. UserDefaults don’t update when you put this key–value-pair into your Info.plist. Thanks Daniel Alm for reporting this!