Multi-line/Autoresizing UITextView similar to SMS-app

by Hans Pinckaers

I’ve been tinkering around the last days, creating a multi-line UITextView. I wanted a SMS-app like experience and needed a growing (and shrinking) textView. I tried using three20′s TTTextEditor, but it disables the bounces of the scroll (which is ugly) and has this big white margin on the bottom when you scroll down manually. So I needed a UITextView which grows/shrinks with the text always on the bottom and a bouncing scroll. This blog post described how it is done. (It is not up to date, this was written in the days of iOS, check GitHub for the latest version)

Growing and Shrinking

I started with the code of Brett Schumann (iPhone Multiline Textbox for SMS style chat). Brett calculates the new height when UITextViewTextDidChangeNotification is posted. He determines the needed height by using sizeWithFont:

CGSize newSize = [textView.text
            sizeWithFont:[UIFont fontWithName:@"Helvetica" size:14]
            constrainedToSize:CGSizeMake(222,9999)
            lineBreakMode:UILineBreakModeWordWrap];

I find this rather inconsistent with other typefaces and sized, because of the inset/padding of the UITextView.

I ended with a much simpler approach, using the textViewDidChange: delegate method. This method is called after inserting the typed character. Determining the height is than as easy as: textView.contentSize.height (UITextView extends UIScrollView)

Determining the height of, for example, one line is done by a hidden UITextView, this is needed for the properties of minNumberOfLines and maxNumberOfLines.

Removing the bottom margin

The first problem that comes about when using a UITextView with one line is that the UITextView scrolls up when focused. With googling you can find the answer for the solution:

textView.contentInset = UIEdgeInsetsZero;

Some people say that the “word-tip” or correction bubble will be cut by the bounds of the UITextview, but I couldn’t reproduce that.

Removing the bottom margin took quite a long time. After a few days of research I found out that UITextView is setting the contentInset on unpredictable times. I tried setting it in several delegate methods but ended up in subclassing UITextView and overriding the method. It seems that this works perfectly.

-(void)setContentInset:(UIEdgeInsets)s
{
	UIEdgeInsets insets = s;

	if(s.bottom>8) insets.bottom = 0;
	insets.top = 0;

	[super setContentInset:insets];
}

Sometimes you want a inset on the bottom when you’re typing and wraps to the next line. But when the user is going to scroll manually you need to set the inset to 0. The solution is overriding setContentOffset:.

-(void)setContentOffset:(CGPoint)s
{
	if(self.tracking || self.decelerating){
		//initiated by user...
		self.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
	} else {

		float bottomOffset = (self.contentSize.height - self.frame.size.height + self.contentInset.bottom);
		if(s.y < bottomOffset && self.scrollEnabled){
			self.contentInset = UIEdgeInsetsMake(0, 0, 8, 0); //maybe use scrollRangeToVisible?
		}

	}

	[super setContentOffset:s];
}

Delegate and UITextView properties

The class uses a internal UITextView, but you can set nearly all properties on a class instance, the will be applied on the internal UITextView. All the UITextView delegate methods are also possible to use.

I also included 2 delegate methods for determining when a grow/shrink starts. willChangeHeight is called within the animation block, so every change you make there on a view gets animated.

The Delegate methods

- (BOOL)growingTextViewShouldBeginEditing:(HPGrowingTextView *)growingTextView;
- (BOOL)growingTextViewShouldEndEditing:(HPGrowingTextView *)growingTextView;

- (void)growingTextViewDidBeginEditing:(HPGrowingTextView *)growingTextView;
- (void)growingTextViewDidEndEditing:(HPGrowingTextView *)growingTextView;

- (BOOL)growingTextView:(HPGrowingTextView *)growingTextView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
- (void)growingTextViewDidChange:(HPGrowingTextView *)growingTextView;

//called WITHIN animation block!
- (void)growingTextView:(HPGrowingTextView *)growingTextView willChangeHeight:(float)height;

//called after animation
- (void)growingTextView:(HPGrowingTextView *)growingTextView didChangeHeight:(float)height;

- (void)growingTextViewDidChangeSelection:(HPGrowingTextView *)growingTextView;
- (BOOL)growingTextViewShouldReturn:(HPGrowingTextView *)growingTextView;

The included properties

int maxNumberOfLines;
int minNumberOfLines;

BOOL animateHeightChange; //default is YES

//UITextView properties
NSString *text;
UIFont *font;
UIColor *textColor;
UITextAlignment textAlignment;
NSRange selectedRange;
BOOL editable;
UIDataDetectorTypes dataDetectorTypes;
UIReturnKeyType returnKeyType;

You can read/write the properties on a class instance. When you need to set a specific property that is not listed here, you can set it directly on the internalTextView. Be careful though; the HPGrowingTextView needs to stay the delegate of the internalTextView!

Download

GrowingTextView

Update: Added fixes suggested in the comments.

License: MIT license

It's a zip file, with the class and an example included.

Please comment if you use it or found any problems.

Too lazy to build the sample project or just want to see if this is what you are looking for? Here is a movie showing the HPGrowingTextView;