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;
[...] 1. A multiline/auto-resizing UITextView element which operates similar to the SMS app. (here) [...]
Thanks a lot!!!
Learned so mach.
Hi,
How do I add a background image of it?
Thanks.
Best,
Dante
@Dante Just an ImageView behind it and backgroundColor on UIColor clearColor. Maybe you should also set the backgroundColor of the internalTextView.
Nice work!
One question, how did you get the Spelling correction pop-up that you can see at :14/:15 of the video.
Glad to have found this. I also was looking for something similar to TTTextEditor but scrolled up to mimic the SMS app on the iPhone.
The only drawback I found with this object is related to the comments. Setting a background image to it. This required a little manual tinkering which would be nice if the object supported it.
I was able to successfully add a stretchable image to the HPGrowingTextView (by inserting as a subview at index 0). I then had to make sure the view’s frame was set correctly in growingTextView. Not bad.
However, when typing and scrolling I could see the text at the top of the text area (the scrolling portion) overlapping the top edge of my background image (the background image is a simple bubble). Upon seeing this, I can see why TTTextEditor left some padding up top (to give the illusion that the text disappeared into the top of the bubble).
[...] Interessante e bastante útil… para o código completo, clica aqui. [...]
@Joshua tap on a word with a red line under it.
Thanks mate!
Helped a lot !
Great work!
Has anyone successfully used this inside an UITableView?
I have problems when I resize the UITableViewCell, namely that the keyboard shortly slides down and then directly up again. This happens everytime the TextView and the TableViewcell resizes.
Olof,
This post here on Stack Overflow shows a fix for that problem: http://stackoverflow.com/questions/4015557/uitextview-in-a-uitableviewcell-smooth-auto-resize-shows-and-hides-keyboard-on-ip
Basically, you need to return NO in:
-(BOOL) textViewShouldEndEditing:(UITextView *)textView
Thanks!!
I will be looking for.
This can really help to fix my problem, thanks!
This is an excellent class, and it has been very helpful! I did have a few issues with it, however:
1) Pasting too much text into the view failed to fire the height change. This turned out to be due to the conditional which prevents grows when the height is over the maximum. I added these lines to the textViewDidChange: method, under if (internalTextView.frame.size.height != newSizeH):
if (newSizeH > maxHeight && internalTextView.frame.size.height <= maxHeight)
newSizeH = maxHeight;
2) The calculated minimum height tended to be incorrect. I solved it by rewriting the setMinNumberOfLines: and setMaxNumberOfLines: methods entirely to use the already-configured internal view, e.g.:
-(void)setMinNumberOfLines:(int)m
{
NSString *saveText = internalTextView.text, *newText = @”-”;
internalTextView.delegate = nil;
internalTextView.hidden = YES;
for (int i = 2; i < m; ++i)
newText = [newText stringByAppendingString:@"\n|W|"];
internalTextView.text = newText;
minHeight = internalTextView.contentSize.height;
internalTextView.text = saveText;
internalTextView.hidden = NO;
internalTextView.delegate = self;
[self sizeToFit];
minNumberOfLines = m;
}
3) The growingTextView:didChangeHeight: delegate method was not called at all when not animating height changes. Adding these lines after [UIView commitAnimations] in -textViewDidChange: solved it:
} else if ([delegate respondsToSelector:@selector(growingTextView:didChangeHeight:)]) {
[delegate growingTextView:self didChangeHeight:newSizeH];
}
4) Finally, my compiler (Clang 2.8) complained about mixing synthesized atomic properties with user-provided setter methods. The solution, since the setters aren’t even remotely atomic anyway (one would have to use thread-safe KVO to make them so), was to simply remove the @synthesize minNumberOfLines,maxNumberOfLines and implement the getters as simple return statements. A recent thread on cocoa-dev discussed this particular compiler warning, the conclusion of was “don’t do that.”
Hope these tweaks are helpful!
Thanks for your tweaks.
You are right that you don’t need a @synthesize when you provide the getter and setter by yourself.
When I find the time I will put your tweaks in the file.
I want to say thank you! that’s what I need indeed! This problem has confused me for a couple of days!
@nick
Thanks! Your tip helped me a lot!
Hi, is it possible to create a multi-line UITextField?
Nope, not that I’m aware of.
Thanks a lot! It works perfectly!
BWT
You can avoid using “NSObject *delegate”
and use “id delegate” just write this @protocol HPGrowingTextViewDelegate
From the bottom of my soul Thank you!!!!!! I
Dude, thanks a lot this is neat. I have been looking for a while now and this is exactly what I wanted I couldnt do it by myself there was always a piece missing
Great job! You’ve saved me days of work!
One question though… How do I set your GrowingTextView to be the first responder. I’ve tried [textView becomeFirstResponder] but it does not get the focus. When I replace your GrowingTextView with a UITextView, that becomes first responder fine.
@Victor Try: [textView.internalTextView becomeFirstResponder];
Hi, This is a really useful class, thanks!
I’ve got a few question want to ask
Can I make the the height of the view shorter so it looks exactly the same as UITextField?
Hi rewrote a couple of methods to get rid of the Depricated warnings on the keyboard bounds under SDK 4.2
- (void)keyboardWillShow:(NSNotification *)notification { NSDictionary *userInfo = [notification userInfo]; NSValue *keyboardBoundsValue = [userInfo objectForKey: UIKeyboardFrameEndUserInfoKey]; [keyboardBoundsValue getValue:&keyboardBounds]; keyboardIsShowing = YES; [self resizeViewControllerToFitScreen];}
- (void)keyboardWillHide:(NSNotification *)note { keyboardIsShowing = NO; keyboardBounds = CGRectMake(0, 0, 0, 0); [self resizeViewControllerToFitScreen];}
- (void)resizeViewControllerToFitScreen { // Needs adjustment for portrait orientation! CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame]; CGRect frame = self.view.frame; frame.size.height = applicationFrame.size.height; CGRect txtMsgFrame = txtMsg.frame; if (keyboardIsShowing) {
//+ and – 40 is the heigt of my navigationbar. frame.size.height -= (keyboardBounds.size.height + 40); txtMsgFrame.origin.y -= keyboardBounds.size.height; } else { frame.size.height -= 40; txtMsgFrame.origin.y += keyboardBounds.size.height; } [UIView beginAnimations:nil context:NULL]; [UIView setAnimationBeginsFromCurrentState:YES]; [UIView setAnimationDuration:0.3f]; self.view.frame = frame; txtMsg.frame = txtMsgFrame; [UIView commitAnimations];}
Ups pasted from xCode with no formatting
well paste the methodes in if its difficult to read
thank you,it’s really useful !
Hi,
Thank you for sharing this code…
have a little question..
I have a tableView and I set HPGrowingTextView under the tableView (like the SMS app)…
I have 2 questions…
when the HPGrowingTextView height changes how could I resize the tableView?
The other question is about the background image. If I refer the SMS app, how could I increase the background image when the HPGrowingTextView also changes?
Thank you once again..
Hi,
I m trying to use the method
- (BOOL)growingTextView:(HPGrowingTextView *)growingTextView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
to limit the number of characters but it never calls.
Any idea?
Hello Sir,
Scrolling not working on preloaded text… How to fix it….. Thanks in advance
@SivaKumar :
I had fixed this issue with some small changes :
1) Class : HPGrowingTextView.m
-(void)setText:(NSString *)atext
{
internalTextView.text= atext;
// include this line to analyze the hieght of the textview.
[self performSelector:@selector(textViewDidChange:) withObject:internalTextView];
}
2) Class : HPGrowingTextView.m
-(void)sizeToFit
{
CGRect r = self.frame;
// check if the text is available in text view or not, if it is available, no need to set it to minimum lenth, it could vary as per the text length
if ([self.text length] > 0) {
return;
}
r.size.height = minHeight;
self.frame = r;
}
Thanks
Awesome work! Worked brilliantly.
A tip to all those trying to use the Three20, TTTextEditor. This is a lot more stable and if you need to style it using Three20′s styling classes just make HPGrowingTextView extend TTView rather than UIView.
Thanks for the great work Hans!
Thanks Hans! This is exactly what I was looking for. I had gotten really close if I left scrolling off, but it went horribly wrong when it was enabled. Seeing where you overrode setContentInset is what did the trick.
Hans
thanks for doing this we are implementing your solution on Soundtracker Radio. check it out on the App Store http://bit.ly/nearbyradio. the update with your cool stuff should be live next week
Why is that constant in the HPTextInternal set to 8? Why not 7 or 9?
This is awesome. I actually used the code for make a module for Titanium Appcelerator.
Here is your code combined with mine:
https://github.com/pec1985/ExpandableTextArea/
I added this:
-(BOOL)becomeFirstResponder{ [super becomeFirstResponder]; return [internalTextView becomeFirstResponder];}
This will show keyboard when user press a button, not only when user touches inside textView. Thanks for this great Class.
Cool. Facebook Messenger uses this.
[...] or article body while entering the text. License: MIT License Author: Hans Pinckaers Site: http://www.hanspinckaers.com/multi-line-uitextview-similar-to-sms Repository: [...]
Thanks man!!!
:):)
I LOVE YOU
Hi Hans,
A short question: i have a table view on my screen and it’s covered by the KB.
Do you know how to make the table scroll all the way up?
Thanks,
S
If I set (instead of typing by keyboard) large text value, it does not expands to the maximum of lines number. Is there any way to expand it?
Also if I set string value containing linefeeds, and then deleting them by keyboard, height goes below 1 line. Any ideas to fix this?
Hi,
Love the implementation so much!!!!
I just have a simple question, how can I make the textField secure? I tried textField.internalTextView.secureTextEntry = YES but it doesn’t change.
Thanks!!!
Super elegant code!
In response to SSR’s question from Feb. – HPGTV looks for this selector, and then handles the “Return” key as needed
// this is a selector that HPGrowingTextView looks for to trap the “Return” key (char \n). Set it to YES to have HPGTV resign the keyboard
- (BOOL)growingTextViewShouldReturn:(id)selector
{
return YES;
}
Hi,
I really love this little piece of code. I only have one problem with it. Scrolling only works if I touch on the first line of the text view. Then i can scroll. Nothing happens when I touch any other line. How can I fix this?
Thanks a lot!
nevermind that… I’ve probably made few too many customizations. I finally got that I didn’t resize appropriate views.
Hello Hans,
Nice work…. I’ m looking for a solution to add image inside textview and then manage selection and text insertion like apple’s mailcomposer does. But I am struggling with this. Do you have any solution?
It looks like it doesn’t resizes to the max height I gave it [4 lines].
Is there a problem with ios5 maybe?
this is my code:
m_textView = [[HPGrowingTextView alloc] init];
m_textView.contentInset = UIEdgeInsetsMake(0, 5, 0, 5);
m_textView.minNumberOfLines = 1;
m_textView.maxNumberOfLines = 4;
m_textView.font = [UIFont systemFontOfSize:14.0];
m_textView.delegate = self;
m_textView.internalTextView.scrollIndicatorInsets = UIEdgeInsetsMake(5, 0, 5, 0);
m_textView.backgroundColor = [UIColor whiteColor];
m_textView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
do you see any thing that might have break it?
It grows all the way to the top.
Thanks,
S
and I’m setting the rect later…