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. Well, I wouldn’t be blogging this if I wouldn’t have been succesful.
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
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;
In iOS 4.3.
“autocomplete text” is out of input frame.
Does anyone has any idea?
Great stuff!
Can you pack the code in the LoadView in the example project into your GrowingTextView? so that can simply implement your delegate methods without writing all the UI?
Hi Hans,
Thanks for this wonderful application.
it save us lots of work & effort.
I just run your application on Xcode 4.2 iOS 5 and it works fine.
However, when I try to copy it on a new project with Story.boared. I notice that I get the first screen, but the keyboard cover the textfield when text field because first responder . I’m sure that I copied all the code and files. but don’t know if I need to do any thing extra on story.boared for example.
any clue why textfield don’t go up and it get covered by keyboard?
thanks
[...] Author site : http://www.hanspinckaers.com/multi-line-uitextview-similar-to-sms [...]
Great job I have to say.
How do you make it to grow upwards instead of downwards?
I modified the example of “resignTextView method , I found the text cannot be automatically ,
iOS 4.3.2
code :
-(void)resignTextView
{
textView.text=[NSString stringWithFormat:@”%@%@”,textView.text,@”
Thanks for the great work!
In my case the text view did not resize correctly when the text was set the first time (maybe because I am using it in a table view?)
I fixed it by adding a zero delay in the setText: method in HPGrowingTextView like this:
-(void)setText:(NSString *)newText
{
internalTextView.text = newText;
// include this line to analyze the height of the textview.
// fix from Ankit Thakur
[self performSelector:@selector(textViewDidChange:) withObject:internalTextView afterDelay:0.0];
}
I like so much your code. It works fine. But when I use it in storyboard, the keyboard cover the textView. Do you have some idea how to resolve it?
I discover why cover the Keyboard. When you create a new project, in AppDelegate create :
“self.viewController =[[[CommentViewController alloc] initWithNibName:@”CommentViewController” bundle:nil] autorelease];”.
But you have to use only “int” to add keyboard’s notification like in:
“self.viewController = [[[CommentViewController alloc] init] autorelease];”. So problem solved
It works great when i am entering text but i want to add image also like mms application. I have been tried using UIImagePickerController. Images are being added into textview. But textview size is not being increased according to image size, and cursor position not moving to image ending position. Could you help me please
There is a github project that implements a multi-line UITextView:
http://github.com/alekperov/ChatInputSample
Amazing job, thanks a lot!
I would like to add to the inputview textview. If you did, they Assignment to readonly property.
txtView.inputView = [UIView alloc] => What do I do?