How do I size a UITextView to its content?

Viewed 414670

Is there a good way to adjust the size of a UITextView to conform to its content? Say for instance I have a UITextView that contains one line of text:

"Hello world"

I then add another line of text:

"Goodbye world"

Is there a good way in Cocoa Touch to get the rect that will hold all of the lines in the text view so that I can adjust the parent view accordingly?

As another example, look at the notes' field for events in the Calendar application - note how the cell (and the UITextView it contains) expands to hold all lines of text in the notes' string.

41 Answers

This no longer works on iOS 7 or above

There is actually a very easy way to do resizing of the UITextView to its correct height of the content. It can be done using the UITextView contentSize.

CGRect frame = _textView.frame;
frame.size.height = _textView.contentSize.height;
_textView.frame = frame;

One thing to note is that the correct contentSize is only available after the UITextView has been added to the view with addSubview. Prior to that it is equal to frame.size

This will not work if auto layout is ON. With auto layout, the general approach is to use the sizeThatFits method and update the constant value on a height constraint.

CGSize sizeThatShouldFitTheContent = [_textView sizeThatFits:_textView.frame.size];
heightConstraint.constant = sizeThatShouldFitTheContent.height;

heightConstraint is a layout constraint that you typically setup via a IBOutlet by linking the property to the height constraint created in a storyboard.


Just to add to this amazing answer, 2014, if you:

[self.textView sizeToFit];

there is a difference in behaviour with the iPhone6+ only:

enter image description here

With the 6+ only (not the 5s or 6) it does add "one more blank line" to the UITextView. The "RL solution" fixes this perfectly:

CGRect _f = self.mainPostText.frame;
_f.size.height = self.mainPostText.contentSize.height;
self.mainPostText.frame = _f;

It fixes the "extra line" problem on 6+.

In my (limited) experience,

- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode

does not respect newline characters, so you can end up with a lot shorter CGSize than is actually required.

- (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size

does seem to respect the newlines.

Also, the text isn't actually rendered at the top of the UITextView. In my code, I set the new height of the UITextView to be 24 pixels larger than the height returned by the sizeOfFont methods.

Did you try [textView sizeThatFits:textView.bounds] ?

Edit: sizeThatFits returns the size but does not actually resize the component. I'm not sure if that's what you want, or if [textView sizeToFit] is more what you were looking for. In either case, I do not know if it will perfectly fit the content like you want, but it's the first thing to try.

Another method is the find the size a particular string will take up using the NSString method:

-(CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size

This returns the size of the rectangle that fits the given string with the given font. Pass in a size with the desired width and a maximum height, and then you can look at the height returned to fit the text. There is a version that lets you specify line break mode also.

You can then use the returned size to change the size of your view to fit.

Using UITextViewDelegate is the easiest way:

func textViewDidChange(_ textView: UITextView) {
    textView.sizeToFit()
    textviewHeight.constant = textView.contentSize.height
}

Combined with Mike McMaster's answer, you might want to do something like:

[myTextView setDelegate: self];

...

- (void)textViewDidChange:(UITextView *)textView {
  if (myTextView == textView) {
     // it changed.  Do resizing here.
  }
}

disable scrolling

add constaints

and add your text

[yourTextView setText:@"your text"];
[yourTextView layoutIfNeeded];

if you use UIScrollView you should add this too;

[yourScrollView layoutIfNeeded];

-(void)viewDidAppear:(BOOL)animated{
    CGRect contentRect = CGRectZero;

    for (UIView *view in self.yourScrollView.subviews) {
         contentRect = CGRectUnion(contentRect, view.frame);
    }
    self.yourScrollView.contentSize = contentRect.size;
}

This works fine for Swift 5 in case you want to fit your TextView once user write text on the fly.

Just implement UITextViewDelegate with:

func textViewDidChange(_ textView: UITextView) {
    let newSize = textView.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
    textView.frame.size = CGSize(width: newSize.width, height: newSize.height)
}

Hope this helps:

- (void)textViewDidChange:(UITextView *)textView {
  CGSize textSize = textview.contentSize;
  if (textSize != textView.frame.size)
      textView.frame.size = textSize;
}

Works like a charm on ios 11 and I'm working in a cell, like a chat cell with bubble.

let content = UITextView(frame: CGRect(x: 4, y: 4, width: 0, height: 0))
content.text = "what ever short or long text you wanna try"
content.textAlignment = NSTextAlignment.left
content.font = UIFont.systemFont(ofSize: 13)
let spaceAvailable = 200 //My cell is fancy I have to calculate it...
let newSize = content.sizeThatFits(CGSize(width: CGFloat(spaceAvailable), height: CGFloat.greatestFiniteMagnitude))
content.isEditable = false
content.dataDetectorTypes = UIDataDetectorTypes.all
content.isScrollEnabled = false
content.backgroundColor = UIColor.clear
bkgView.addSubview(content)

It's quite easy with Key Value Observing (KVO), just create a subclass of UITextView and do:

private func setup() { // Called from init or somewhere

    fitToContentObservations = [
        textView.observe(\.contentSize) { _, _ in
            self.invalidateIntrinsicContentSize()
        },
        // For some reason the content offset sometimes is non zero even though the frame is the same size as the content size.
        textView.observe(\.contentOffset) { _, _ in
            if self.contentOffset != .zero { // Need to check this to stop infinite loop
                self.contentOffset = .zero
            }
        }
    ]
}
public override var intrinsicContentSize: CGSize {
    return contentSize
}

If you don't want to subclass you could try doing textView.bounds = textView.contentSize in the contentSize observer.

The simplest solution that worked for me was to put a height constraint on the textView in the Storyboard, then connect the textView and height constraint to the code:

@IBOutlet var myAwesomeTextView: UITextView!
@IBOutlet var myAwesomeTextViewHeight: NSLayoutConstraint!

Then after setting the text and paragraph styles, add this in viewDidAppear:

self.myAwesomeTextViewHeight.constant = self.myAwesomeTextView.contentSize.height

Some notes:

  1. In contrast to other solutions, isScrollEnabled must to be set to true in order for this to work.
  2. In my case, I was setting custom attributes to the font inside the code, therefore I had to set the height in viewDidAppear (it wouldn't work properly before that). If you aren't changing any text attributes in code, you should be able to set the height in viewDidLoad or anywhere after setting the text.

Here are the steps

Solution works on all versions of iOS. Swift 3.0 and above.

  1. Add your UITextView to the View. I'm adding it with code, you can add via interface builder.
  2. Add constraints. I'm adding the constraints in code, you can do it in interface builder as well.
  3. Use UITextViewDelegate method func textViewDidChange(_ textView: UITextView) to adjust the size of the TextView

Code :

  1. //1. Add your UITextView in ViewDidLoad
    let textView = UITextView()
    textView.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
    textView.backgroundColor = .lightGray
    textView.text = "Here is some default text."
    
    
    
    //2. Add constraints
    textView.translatesAutoresizingMaskIntoConstraints = false
    [
    textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
    textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    textView.heightAnchor.constraint(equalToConstant: 50),
    textView.widthAnchor.constraint(equalToConstant: 30)
    ].forEach{ $0.isActive = true }
    
    textView.font = UIFont.preferredFont(forTextStyle: .headline)
    
    textView.delegate = self
    textView.isScrollEnabled = false
    
    textViewDidChange(textView)
    
    
    //3. Implement the delegate method.
    func textViewDidChange(_ textView: UITextView) {
    let size = CGSize(width: view.frame.width, height: .infinity)
    let estimatedSize = textView.sizeThatFits(size)
    
    textView.constraints.forEach { (constraint) in
        if constraint.firstAttribute == .height {
            print("Height: ", estimatedSize.height)
            constraint.constant = estimatedSize.height
        }
    }
    }
    
Related