· 3 Min read

Auto-resizing UITextView with Auto-Layout

Post

Building a messaging application sounds fun, but it can quickly become quite challenging. One of the first difficulties you might encounter is the UITextView the user types in, found on the chat page.

The expected behaviour is that the Text View has an initial size that grows bigger as the user is typing, up until an established fixed height, then scrolls afterwards.

And that’s exactly what we are trying to achieve. Let’s get started! 🏁

1. Initial setup

  let textView = UITextView()
  textView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
  textView.font = UIFont.preferredFont(forTextStyle: .body)
  textView.isScrollEnabled = false
  view.addSubview(textView)
 
  // Add constraints to pin textView at the bottom of the screen
  textView.translatesAutoresizingMaskIntoConstraints = false
  textView.heightAnchor.constraint(equalToConstant: textViewMinHeight)
  textView.delegate = self

Firstly, we give the text some insets so it’s more readable. Since the TextView shouldn’t scroll in its initial state, we need to disable it by setting isScrollEnabled property to false.

We add our textView to the view hierarchy, pin it at the bottom and give it an initial height using constraints. The textViewMinHeight can be any value you desire or you can calculate the needed height for exactly one line this way:

  let textViewPadding: CGFloat = 10
  let textViewFontHeight = messageTextView.font?.lineHeight ?? 20
  let verticalPadding = 2 * textViewPadding 
  textViewMinHeight = textViewFontHeight + verticalPadding
  textViewMaxHeight = (textViewFontHeight * maxLinesDisplayed) + verticalPadding + 10

The minimum height in this case is the TextView’s font line height (which is an optional), to which was added the top & bottom insets set previously (here 10).

For the maximum height, depending how many lines we want to show, we multiply the number of lines (here 3) with TextView’s font line height, we add again the vertical spacing and also the space between the lines. Having only 3 lines, that means that there are 2 spaces with a value of 5 each, so we add the value 10 that that’s our final height.

In order to adjust the spacing between the text lines, you need to create a NSMutableParagraphStyle and add it as an attribute to the text view’s attributedText property. I won’t go into detail about implementing that, a simple Google search will clear all out.

Finally, we must make our ViewController confirm to UITextViewDelegate and we set the textview’s delegate to self. That’s what we’ll do next.

2. Listening to text change

extension ChatViewController: UITextViewDelegate {
   func textViewDidChange(_ textView: UITextView) { 
     // 1
     let size = CGSize(width: textView.frame.width, height: .infinity)
     let estimatedSize = textView.sizeThatFits(size)
     guard estimatedSize.height <= textViewMaxHeight else {
       textView.isScrollEnabled = true
       return
     }
 
   // 2
   textView.constraints.forEach { constraint in
      if constraint.firstAttribute == .height {
        guard constraint.constant != estimatedSize.height else {
           return
        }
        // Disable the scroll
        textView.isScrollEnabled = false
        if estimatedSize.height < textViewMinHeight {
          constraint.constant = textViewMinHeight
        } else {
          constraint.constant = estimatedSize.height
        }
     }
   }
}

At the bottom of our ViewController, we conform to UITextViewDelegate and implement the textViewDidChange delegate method.

We break this in two parts for clarity:

Part 1

  1. We create a CGSize instance with the textView’s frame’s width and infinity passed as the height parameter. Here we can use any big number as the height, it doesn’t matter because next we call sizeThatFits method on the previously created size.

  2. According to the Apple docs, this method “asks the view to calculate and return the size that best fits the specified size”.

  3. Then, in order to continue, we check if the estimated size is smaller than our textViewMaxHeight. If not, then we’ve reached the maximum height so we just enable the scroll and return.

Part 2

  1. We loop through the textView’s constraints to find the one that corresponds to the height.

  2. Once we find it, we check if we don’t already have the desired height set up. This in an optional step, meant to reduce executions, the delegate method being executed for every character change.

  3. We double check that the estimated size is not less that our minimum height threshold, and correct it if necessary.

  4. Otherwise, we just update the constraint value to the estimate value.

That’s it. Sounds simple, right?

Prior to tackling a new concept, every little detail seems overwhelming, like adventuring in the unknown. But once the implementation is finished, you realise you had to follow the map all along 🗺️.

Thank you for reading! 🚀