UITextView
My recent blog posts have been more business-y than programmer-y, so today we're going to bring back that codin' feelin'. With a twist, since I'm going to talk about iOS, which is blasphemy in these parts, but don't shout. I'm writing an iOS version of Underpass, and I needed a UITextView
that automatically resizes vertically like in Messages. Searching the web reveals that, give or take, a million other developers want this too. Unfortunately, Apple does not provide an API for the behavior, instead forcing developers to figure it out for themselves. As a result, a million developers have produced a million subtly different implementations. The paradox of choice is that I couldn't choose one. To stave off death from both hunger and thirst, I decided to write my own millionth and one implementation.
I should also mention that I'm using autolayout. My goal was to let autolayout do most of the work. Shouldn't autolayout do most of the work? That's supposed to be one of the advantages of autolayout. And it turns out to be quite easy to create an automatically resizing UITextView
using only autolayout constraints. The catch, though, is that such a text view must be non-scrollable. Here's a passage from Apple's "Auto Layout Guide":
A text view’s intrinsic content size varies depending on the content, on whether or not it has scrolling enabled, and on the other constraints applied to the view. For example, with scrolling enabled, the view does not have an intrinsic content size. With scrolling disabled, by default the view’s intrinsic content size is calculated based on the size of the text without any line wrapping. For example, if there are no returns in the text, it calculates the height and width needed to layout the content as a single line of text. If you add constraints to specify the view’s width, the intrinsic content size defines the height required to display the text given its width.
The problem is that if you want an editable text view to have a maximum height, the text view can't be non-scrollable, because the user content can outgrow the maximum. This is especially problematic if the user can paste images into the text view (allowsEditingTextAttributes
). So what's the one in a million solution?
Update: After I posted this article, I was given a better solution to the problem by Rob Mayoff. Now we have a million and two implementations. See the Appendix at the end.
My idea was that as long as the text view can grow to fit the entire content, it doesn't need to be scrollable. It only needs to be scrollable after reaching its maximum height and the content is too big to fit. With that idea, I wrote a simple subclass of UITextView
, because the simplest code is (usually) the best code. Below is the subclass in all its glory. (Yes, I'm writing a new app in Objective-C.)
-(void)invalidateIntrinsicContentSize
{
[self setScrollEnabled:NO]; // UITextView intrinsicContentSize only exists if scrolling is disabled
[super invalidateIntrinsicContentSize];
}
-(void)layoutSubviews
{
[super layoutSubviews];
CGRect bounds = [self bounds];
CGSize fittingSize = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
if (fittingSize.height > CGRectGetHeight(bounds))
[self setScrollEnabled:YES];
}
To make it work, I create an instance of my UITextView
subclass and set its autolayout constraints. In my case, I constrain its leading, trailing, and bottom anchors to the superview. I constrain the top anchor to the bottom anchor of the view above it. The other views in the superview have their own height constraints, so that effectively gives the text view a maximum height. You'll most likely also want a minimum height, so use a greater than or equal to constant height constraint for that. Finally, I call invalidateIntrinsicContentSize
in the UITextViewDelegate
method textViewDidChange:
so that the text view height is recalculated every time the user changes the content.
That's all for now. Maybe next time I'll tell you my trick for getting NSTextAttachment
images to automatically resize to fit within the UITextView
width. I could be persuaded if you buy 1000 copies of my app. Or cut out the middleman and send me a check.
After I posted this article, I was given a better solution to the problem by Rob Mayoff. Rob suggested that I put a non-scrollable UITextView
inside a scrollable UIScrollView
. The trick here is that you need to create an extra constraint that sets the height of the scroll view equal to the height of the text view, but you give the constraint a lower priority than the other constraints. Then the text view will resize with its content because it's non-scrollable, and autolayout will attempt to match the scroll view's height to the text view, but the other constraints that restrict the scroll view's maximum height will have a higher priority and prevent the scroll view from growing forever.
The advantage of Rob's approach is that autolayout does all the work. You don't have to subclass UITextView
. You don't even have to call invalidateIntrinsicContentSize
. Thus, I consider this solution to be superior to mine. Thanks, Rob!