smr
smr

Reputation: 1243

NSTableView reloadData(forRowIndexes:columnIndexes:) breaks autolayout

I have an NSTableView that can swap in different cell views based on data values for the row. When the model changes, I reload the table, and the table's delegate will provide the right table cell view for the new data.

The table uses autolayout for its cell views. All cell views load normally initially. When updating the table after a model change, I get different results depending on whether I call reloadData() or reloadData(forRowIndexes:columnIndexes). When using reloadData(), the cell view is loaded and autolayout works fine. If I use reloadData(forRowIndexes:columnIndexes), autolayout produces completely different, unexpected results.

I created a sample project to demonstrate the problem. Here is an image of the project setup including constraints set on the table cell views. There are two row templates, one with a blue view (even rows), one with green (odd rows) that should span the table width (minus a bit of padding). A controller supplies the cell views:

class TableController: NSObject {
    @IBOutlet weak var tableView: NSTableView!
    var colorData = [1, 0, 1, 0]

    @IBAction func swapLine(_ sender: Any) {
        colorData[1] = (colorData[1] + 1) % 2
    //        tableView.reloadData()
        tableView.reloadData(forRowIndexes: [1], columnIndexes: [0])
    }
}

extension TableController: NSTableViewDataSource {
    func numberOfRows(in tableView: NSTableView) -> Int {
        return colorData.count
    }
}

extension TableController: NSTableViewDelegate {
    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
        let cellId = (colorData[row]) % 2 == 0 ? "EvenCell" : "OddCell"
        return tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(cellId), owner: self)
    }
}

A button in the interface just swaps the data for row 1 and reloads the data. The initial view looks like this (alternating green and blue rects). If you use reloadData(), it looks like this (row 1 changed from blue to green). But, if you use reloadData(withRowIndexes:columnIndexes:), the cell view shrinks to 40 points wide vice 480 as in the others. Here's a grab of the view debugger showing the cell view with the wrong size and showing ambiguous width constraints (this doesn't happen when using reloadData()).

The documentation mentions that the row view is reused with reloadData(forRowIndexes:columnIndexes:), but not with reloadData(), which I've verified. I imagine this reusing of the row view is what's causing the autolayout problems, but I can find no connection. Nothing found at SO, AppKit release notes, WWDC videos, Google searches or from pounding my head on the table. Would be truly grateful for assistance.

Update: Here's the code for ColorView:

class ColorView: NSView {
    @IBInspectable var intrinsicHeight: CGFloat = 20
    @IBInspectable var color: NSColor = NSColor.blue

    override var intrinsicContentSize: NSSize {
        return NSSize(width: NSView.noIntrinsicMetric, height: intrinsicHeight)
    }

    override func draw(_ dirtyRect: NSRect) {
        color.setFill()
        dirtyRect.fill()
    }
}

Upvotes: 4

Views: 1000

Answers (2)

sjaq
sjaq

Reputation: 105

I ran into the same issue, and noticed the actual auto-layout constraints were missing for the rows that reloadData is called for. My (hacky) solution was to add the constraints that are supposed to be automatically set up for the cell manually as well. Note that in my table view I'm just using one column so I'm able to set the width constraint to equal the row's width instead of relying on the columns specified width.

class CustomRowView: NSTableRowView {
  override func addSubview(_ view: NSView) {
    super.addSubview(view)

    // Add constraints NSTableView is supposed to set up
    view.topAnchor.constraint(equalTo: topAnchor).isActive = true
    view.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
    view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    view.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0).isActive = true
    view.layoutSubtreeIfNeeded()
  }
}

Upvotes: 0

Clifton Labrum
Clifton Labrum

Reputation: 14168

I think I've got it working. If I call layoutSubtreeIfNeeded() on the cell just before it is returned (so that all its subviews like the dynamic text are already set), then it seems to work.

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
  //...

  cell.layoutSubtreeIfNeeded()
  return cell
}

I hope that helps.

Upvotes: 1

Related Questions