Search code examples
autolayoutnstableviewnstextfieldappkit

Editable NSTextFields with Variable Height in NSTableView Rows in macOS App


Xcode 10.1, Swift 4.2, macOS 10.14.2

I am trying to make a simple to do list app for macOS where there are a series of NSTableView rows and inside each one is an NSTextField. Each field is a to-do item. I want the NSTableView rows to expand to fit the size of the text within each NSTextField.

I have all of the following working:

  1. Setting the text in the NSTextField makes the NSTableView row expand as needed. Auto layout constraints are set in my storyboard.
  2. Using tableView.reloadData(forRowIndexes: ..., columnIndexes: ...) sets the text and resizes the table row correctly. reloadData works But doing tableView.reloadData() always resets every NSTextField to a single line of text as shown here: reloadData fails Interestingly, if you click into the NSTextField after reloading the whole table, the field resizes to fit its content again: Clicking a row fixes it

I believe I have set all the appropriate auto layout constraints on my NSTextField and I'm using a custom subclass for it as well (from a helpful answer here):

class FancyField: NSTextField{
  override var intrinsicContentSize: NSSize {
    // Guard the cell exists and wraps
    guard let cell = self.cell, cell.wraps else {return super.intrinsicContentSize}

    // Use intrinsic width to jibe with autolayout
    let width = super.intrinsicContentSize.width

    // Set the frame height to a reasonable number
    self.frame.size.height = 150.0

    // Calcuate height
    let height = cell.cellSize(forBounds: self.frame).height

    return NSMakeSize(width, height)
  }

  override func textDidChange(_ notification: Notification) {
    super.textDidChange(notification)
    super.invalidateIntrinsicContentSize()
  }
}

⭐️Here is a sample project: https://d.pr/f/90CTEh

I'm at a loss as to what else I can try. Any ideas?


Solution

  • I think there is some basic problems with the constraints in interface builder. Resizing the window makes everything wonky. Also you should call validateEditing() in the textDidChange(forBounds:) in your FancyField class. I created a sample project, that does what you want on Github

    Write a comment if you have any problems with it.

    Thinking a little about it, thought i would add the meat of the code here. Only thing that really needs to work, is the update on the NSTextField when the "Tasks" is being updated. Following is the code required for the NSTextField.

    public class DynamicTextField: NSTextField {
       public override var intrinsicContentSize: NSSize {
          if cell!.wraps {
             let fictionalBounds = NSRect(x: bounds.minX, y: bounds.minY, width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
             return cell!.cellSize(forBounds: fictionalBounds)
          } else {
             return super.intrinsicContentSize
          }
       }
    
       public override func textDidChange(_ notification: Notification) {
          super.textDidChange(notification)
    
          if cell!.wraps {
             validatingEditing()
             invalidateIntrinsicContentSize()
          }
       }
    }
    

    Hope it helps.