Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 577
Clifton Labrum Avatar asked Nov 08 '17 23:11

Clifton Labrum


1 Answers

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
    }
  }
}
like image 96
Clifton Labrum Avatar answered Nov 04 '22 15:11

Clifton Labrum