Search code examples
cocoaautolayoutnstextfield

NSTextField That Grows Taller Using Swift and Auto Layout


Xcode 9.1, Swift 4

I'm trying to create an NSTextField that grows in height as the user enters text. Just like the one in iMessage on a Mac. Here's an example video: http://d.pr/v/zWRA6w

I have set up the NSViews like this so that I can put a custom design around the NSTextField and just leave its default border and background off:

enter image description here

Here are my constraints. The chat conversation scrolls underneath the chat wrap. enter image description here enter image description here enter image description here

I tried to follow this answer and created the following Swift version:

class ResizingTextField: NSTextField{
  var isEditing = false
  override func textDidBeginEditing(_ notification: Notification) {
    super.textDidBeginEditing(notification)
    isEditing = true
  }
  override func textDidEndEditing(_ notification: Notification) {
    super.textDidEndEditing(notification)
    isEditing = false
  }
  override func textDidChange(_ notification: Notification) {
    super.textDidChange(notification)
    self.invalidateIntrinsicContentSize()
  }
  override public var intrinsicContentSize: CGSize {
    if isEditing{
      let fieldEditor = self.window?.fieldEditor(false, for: self)
      if fieldEditor != nil{
        if let cellCopy = self.cell?.copy() as? NSTextFieldCell{
          cellCopy.stringValue = fieldEditor!.string
          return cellCopy.cellSize
        }
      }
    }
    return self.cell!.cellSize
  }
}

But there must be something wrong with my constraints and/or code, as nothing happens when I type in the box.

Any suggestions?


Solution

  • I came across this which worked: https://gist.github.com/entotsu/ddc136832a87a0fd2f9a0a6d4cf754ea

    I had to update the code a bit to work with Swift 4:

    class AutoGrowingTextField: NSTextField {
    
      var minHeight: CGFloat? = 22
      let bottomSpace: CGFloat = 7
      // magic number! (the field editor TextView is offset within the NSTextField. It’s easy to get the space above (it’s origin), but it’s difficult to get the default spacing for the bottom, as we may be changing the height
    
      var heightLimit: CGFloat?
      var lastSize: NSSize?
      var isEditing = false
    
      override func textDidBeginEditing(_ notification: Notification) {
        super.textDidBeginEditing(notification)
        isEditing = true
      }
      override func textDidEndEditing(_ notification: Notification) {
        super.textDidEndEditing(notification)
        isEditing = false
      }
      override func textDidChange(_ notification: Notification) {
        super.textDidChange(notification)
        self.invalidateIntrinsicContentSize()
      }
    
      override var intrinsicContentSize: NSSize {
        var minSize: NSSize {
          var size = super.intrinsicContentSize
          size.height = minHeight ?? 0
          return size
        }
        // Only update the size if we’re editing the text, or if we’ve not set it yet
        // If we try and update it while another text field is selected, it may shrink back down to only the size of one line (for some reason?)
        if isEditing || lastSize == nil {
    
          //If we’re being edited, get the shared NSTextView field editor, so we can get more info
          guard let textView = self.window?.fieldEditor(false, for: self) as? NSTextView, let container = textView.textContainer, let newHeight = container.layoutManager?.usedRect(for: container).height
          else {
              return lastSize ?? minSize
          }
          var newSize = super.intrinsicContentSize
          newSize.height = newHeight + bottomSpace
    
          if let heightLimit = heightLimit, let lastSize = lastSize, newSize.height > heightLimit {
            newSize = lastSize
          }
    
          if let minHeight = minHeight, newSize.height < minHeight {
            newSize.height = minHeight
          }
    
          lastSize = newSize
          return newSize
        }
        else {
          return lastSize ?? minSize
        }
      }
    }