macosnsoutlineviewnstablecellview

NSOutlineView - Get NSTableCellView for a Given Item


My view-based outline view displays a custom context menu (right-click menu) for its rows.

One of the menu items on the menu is "Rename...", and the menu item's representedObject property is set to the object represented by the outline view row:

    let menu = NSMenu()

    // ...other menu items...

    let renameItem = NSMenuItem(
            title: "Rename...",
            action: #selector(OutlineViewController.rename(_:)),
            keyEquivalent: "")
    renameItem.representedObject = object

    menu.addItem(renameItem)

On the action side, I want to make the text field in the table cell editable, programmatically. The problem is, I am not sure of how to get a reference to the table cell from the represented object alone.

This is my action method:

@IBAction func rename(_ sender: Any) {
    guard let menuItem = sender as? NSMenuItem else { return }
    guard let item = menuItem.representedObject else { return }

I can get the row for the represented object (Int):

    let row = outlineView.row(forItem: item)

...and the row view (NSTableRowView):

    let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false)

I can get the column index (Int) and column (NSTableColumn):

    let columnIndex = outlineView.column(withIdentifier: "TitleColumn")
    let column = outlineView.tableColumns[columnIndex]

...and attempt to get the cell view (NSTableCellView):

    guard let cell = outlineView(outlineView, viewFor: column, item: item) as? NSTableCellView else {
        return
    }

Finally, I try to make the text field editable:

    guard let textField = cell.textField else {
        return
    }
    textField.becomeFirstResponder()

All these test pass (I set breakpoints), but nothing happens: The text field does not become editable (unlike when double clicking).

What am I doing wrong?


Edit I have realized that when I call:

outlineView(outlineView, viewFor: column, item: item)

This is basically the NSOutlineViewDelegate method that my view controller is implementing, so instead of giving me the cell already on screen, it is creating a new copy on demand. Kind of like calling

UITableViewController.tableView(_:,cellForRowAt:)
// Data source method, creates or dequeues a new cell to pass 
// back to the table for display 

...instead of calling:

UITableView.cellForRowAt(_:) // Method of table view proper; returns existing cell if // already on screen, or defers to the data source (method // above) otherwise

So instead of calling a delegate method that creates the cell view anew, I should query the outline view itself. However, the only meaningful method I can see is:

func rowView(atRow row: Int, makeIfNecessary: Bool) -> NSTableRowView?

...which returns an NSTableRowView...


Solution

  • Solved it. Just had to dig a little deeper into the API:

    // Get row index
    let row = outlineView.row(forItem: item)
    
    // Get view for the whole row: 
    guard let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false) else {
        return
    }
    
    // THIS is the missing piece: Get row subview for the given column
    // (= cell)  
    guard let cell = rowView?.view(atColumn: 0) as? NSTableCellView else {
        return
    }
    

    Now I have a reference to the actual text field already on screen, and it becomes editable:

    self.view.window?.makeFirstResponder(cell.textField)