// // HPTextView.m // // Created by Hans Pinckaers on 29-06-10. // // MIT License // // Copyright (c) 2011 Hans Pinckaers // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #import "HPGrowingTextView.h" #import "HPTextViewInternal.h" @interface HPGrowingTextView(private) -(void)commonInitialiser; -(void)resizeTextView:(NSInteger)newSizeH; -(void)growDidStop; @end @implementation HPGrowingTextView @synthesize internalTextView; @synthesize delegate; @synthesize maxHeight; @synthesize minHeight; @synthesize font; @synthesize textColor; @synthesize textAlignment; @synthesize selectedRange; @synthesize editable; @synthesize dataDetectorTypes; @synthesize animateHeightChange; @synthesize animationDuration; @synthesize returnKeyType; @dynamic placeholder; @dynamic placeholderColor; @synthesize textViewText; // having initwithcoder allows us to use HPGrowingTextView in a Nib. -- aob, 9/2011 - (id)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { [self commonInitialiser]; } return self; } - (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { [self commonInitialiser]; } return self; } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 - (id)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer { if ((self = [super initWithFrame:frame])) { [self commonInitialiser:textContainer]; } return self; } -(void)commonInitialiser { [self commonInitialiser:nil]; } -(void)commonInitialiser:(NSTextContainer *)textContainer #else -(void)commonInitialiser #endif { // Initialization code CGRect r = self.frame; r.origin.y = 0; r.origin.x = 0; #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 internalTextView = [[HPTextViewInternal alloc] initWithFrame:r textContainer:textContainer]; #else internalTextView = [[HPTextViewInternal alloc] initWithFrame:r]; #endif internalTextView.delegate = self; internalTextView.scrollEnabled = NO; internalTextView.font = [UIFont fontWithName:@"Helvetica" size:13]; internalTextView.contentInset = UIEdgeInsetsZero; internalTextView.showsHorizontalScrollIndicator = NO; internalTextView.text = @"-"; internalTextView.contentMode = UIViewContentModeRedraw; [self addSubview:internalTextView]; minHeight = internalTextView.frame.size.height; minNumberOfLines = 1; animateHeightChange = YES; animationDuration = 0.1f; internalTextView.text = @""; [self setMaxNumberOfLines:3]; [self setPlaceholderColor:[UIColor lightGrayColor]]; internalTextView.displayPlaceHolder = YES; } -(CGSize)sizeThatFits:(CGSize)size { if (self.text.length == 0) { size.height = minHeight; } return size; } -(void)layoutSubviews { [super layoutSubviews]; CGRect r = self.bounds; r.origin.y = 0; r.origin.x = contentInset.left; r.size.width -= contentInset.left + contentInset.right; internalTextView.frame = r; } -(void)setContentInset:(UIEdgeInsets)inset { contentInset = inset; CGRect r = self.frame; r.origin.y = inset.top - inset.bottom; r.origin.x = inset.left; r.size.width -= inset.left + inset.right; internalTextView.frame = r; [self setMaxNumberOfLines:maxNumberOfLines]; [self setMinNumberOfLines:minNumberOfLines]; } -(UIEdgeInsets)contentInset { return contentInset; } -(void)setMaxNumberOfLines:(int)n { if(n == 0 && maxHeight > 0) return; // the user specified a maxHeight themselves. // Use internalTextView for height calculations, thanks to Gwynne NSString *saveText = internalTextView.text, *newText = @"-"; internalTextView.delegate = nil; internalTextView.hidden = YES; for (int i = 1; i < n; ++i) newText = [newText stringByAppendingString:@"\n|W|"]; internalTextView.text = newText; maxHeight = [self measureHeight]; internalTextView.text = saveText; internalTextView.hidden = NO; internalTextView.delegate = self; [self sizeToFit]; maxNumberOfLines = n; } -(int)maxNumberOfLines { return maxNumberOfLines; } - (void)setMaxHeight:(int)height { maxHeight = height; maxNumberOfLines = 0; } -(void)setMinNumberOfLines:(int)m { if(m == 0 && minHeight > 0) return; // the user specified a minHeight themselves. // Use internalTextView for height calculations, thanks to Gwynne NSString *saveText = internalTextView.text, *newText = @"-"; internalTextView.delegate = nil; internalTextView.hidden = YES; for (int i = 1; i < m; ++i) newText = [newText stringByAppendingString:@"\n|W|"]; internalTextView.text = newText; minHeight = [self measureHeight]; internalTextView.text = saveText; internalTextView.hidden = NO; internalTextView.delegate = self; [self sizeToFit]; minNumberOfLines = m; } -(int)minNumberOfLines { return minNumberOfLines; } - (void)setMinHeight:(int)height { minHeight = height; minNumberOfLines = 0; } - (NSString *)placeholder { return internalTextView.placeholder; } -(void)deleteBackward{ [internalTextView deleteBackward]; } - (void)setPlaceholder:(NSString *)placeholder { [internalTextView setPlaceholder:placeholder]; [internalTextView setNeedsDisplay]; } - (UIColor *)placeholderColor { return internalTextView.placeholderColor; } - (void)setPlaceholderColor:(UIColor *)placeholderColor { [internalTextView setPlaceholderColor:placeholderColor]; } - (void)textViewDidChange:(UITextView *)textView { if ([textView.text isEqualToString:self.textViewText]) { return; } self.textViewText = textView.text; [self refreshHeight]; } - (void)refreshHeight { //size of content, so we can set the frame of self NSInteger newSizeH = [self measureHeight]; if (newSizeH < minHeight || !internalTextView.hasText) { newSizeH = minHeight; //not smalles than minHeight } else if (maxHeight && newSizeH > maxHeight) { newSizeH = maxHeight; // not taller than maxHeight } if (internalTextView.frame.size.height != newSizeH) { // if our new height is greater than the maxHeight // sets not set the height or move things // around and enable scrolling if (newSizeH >= maxHeight) { if(!internalTextView.scrollEnabled){ internalTextView.scrollEnabled = YES; [internalTextView flashScrollIndicators]; } } else { internalTextView.scrollEnabled = NO; } // [fixed] Pasting too much text into the view failed to fire the height change, // thanks to Gwynne if (newSizeH <= maxHeight) { if(animateHeightChange) { if ([UIView resolveClassMethod:@selector(animateWithDuration:animations:)]) { #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000 [UIView animateWithDuration:animationDuration delay:0 options:(UIViewAnimationOptionAllowUserInteraction| UIViewAnimationOptionBeginFromCurrentState) animations:^(void) { [self resizeTextView:newSizeH]; } completion:^(BOOL finished) { if ([delegate respondsToSelector:@selector(growingTextView:didChangeHeight:)]) { [delegate growingTextView:self didChangeHeight:newSizeH]; } }]; #endif } else { [UIView beginAnimations:@"" context:nil]; [UIView setAnimationDuration:animationDuration]; [UIView setAnimationDelegate:self]; [UIView setAnimationDidStopSelector:@selector(growDidStop)]; [UIView setAnimationBeginsFromCurrentState:YES]; [self resizeTextView:newSizeH]; [UIView commitAnimations]; } } else { [self resizeTextView:newSizeH]; // [fixed] The growingTextView:didChangeHeight: delegate method was not called at all when not animating height changes. // thanks to Gwynne if ([delegate respondsToSelector:@selector(growingTextView:didChangeHeight:)]) { [delegate growingTextView:self didChangeHeight:newSizeH]; } } } } // Display (or not) the placeholder string BOOL wasDisplayingPlaceholder = internalTextView.displayPlaceHolder; internalTextView.displayPlaceHolder = self.internalTextView.text.length == 0; if (wasDisplayingPlaceholder != internalTextView.displayPlaceHolder) { [internalTextView setNeedsDisplay]; } // scroll to caret (needed on iOS7) if ([self respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)]) { [self performSelector:@selector(resetScrollPositionForIOS7) withObject:nil afterDelay:0.1f]; } // Tell the delegate that the text view changed if ([delegate respondsToSelector:@selector(growingTextViewDidChange:)]) { [delegate growingTextViewDidChange:self]; } } // Code from apple developer forum - @Steve Krulewitz, @Mark Marszal, @Eric Silverberg - (CGFloat)measureHeight { if ([self respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)]) { return ceilf([self.internalTextView sizeThatFits:self.internalTextView.frame.size].height); } else { return self.internalTextView.contentSize.height; } } - (void)resetScrollPositionForIOS7 { CGRect r = [internalTextView caretRectForPosition:internalTextView.selectedTextRange.end]; CGFloat caretY = MAX(r.origin.y - internalTextView.frame.size.height + r.size.height + 8, 0); if (internalTextView.contentOffset.y < caretY && r.origin.y != INFINITY) internalTextView.contentOffset = CGPointMake(0, caretY); } -(void)resizeTextView:(NSInteger)newSizeH { if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:)]) { [delegate growingTextView:self willChangeHeight:newSizeH]; } CGRect internalTextViewFrame = self.frame; internalTextViewFrame.size.height = newSizeH; // + padding self.frame = internalTextViewFrame; internalTextViewFrame.origin.y = contentInset.top - contentInset.bottom; internalTextViewFrame.origin.x = contentInset.left; if(!CGRectEqualToRect(internalTextView.frame, internalTextViewFrame)) internalTextView.frame = internalTextViewFrame; } - (void)growDidStop { // scroll to caret (needed on iOS7) if ([self respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)]) { [self resetScrollPositionForIOS7]; } if ([delegate respondsToSelector:@selector(growingTextView:didChangeHeight:)]) { [delegate growingTextView:self didChangeHeight:self.frame.size.height]; } } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [internalTextView becomeFirstResponder]; } - (BOOL)becomeFirstResponder { [super becomeFirstResponder]; return [self.internalTextView becomeFirstResponder]; } -(BOOL)resignFirstResponder { [super resignFirstResponder]; return [internalTextView resignFirstResponder]; } -(BOOL)isFirstResponder { return [self.internalTextView isFirstResponder]; } /////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark UITextView properties /////////////////////////////////////////////////////////////////////////////////////////////////// -(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]; } -(NSString*) text { return internalTextView.text; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setFont:(UIFont *)afont { internalTextView.font= afont; [self setMaxNumberOfLines:maxNumberOfLines]; [self setMinNumberOfLines:minNumberOfLines]; } -(UIFont *)font { return internalTextView.font; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setTextColor:(UIColor *)color { internalTextView.textColor = color; } -(UIColor*)textColor{ return internalTextView.textColor; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setBackgroundColor:(UIColor *)backgroundColor { [super setBackgroundColor:backgroundColor]; internalTextView.backgroundColor = backgroundColor; } -(UIColor*)backgroundColor { return internalTextView.backgroundColor; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setTextAlignment:(NSTextAlignment)aligment { internalTextView.textAlignment = aligment; } -(NSTextAlignment)textAlignment { return internalTextView.textAlignment; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setSelectedRange:(NSRange)range { internalTextView.selectedRange = range; } -(NSRange)selectedRange { return internalTextView.selectedRange; } /////////////////////////////////////////////////////////////////////////////////////////////////// - (void)setIsScrollable:(BOOL)isScrollable { internalTextView.scrollEnabled = isScrollable; } - (BOOL)isScrollable { return internalTextView.scrollEnabled; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setEditable:(BOOL)beditable { internalTextView.editable = beditable; } -(BOOL)isEditable { return internalTextView.editable; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setReturnKeyType:(UIReturnKeyType)keyType { internalTextView.returnKeyType = keyType; } -(UIReturnKeyType)returnKeyType { return internalTextView.returnKeyType; } /////////////////////////////////////////////////////////////////////////////////////////////////// - (void)setKeyboardType:(UIKeyboardType)keyType { internalTextView.keyboardType = keyType; } - (UIKeyboardType)keyboardType { return internalTextView.keyboardType; } /////////////////////////////////////////////////////////////////////////////////////////////////// - (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically { internalTextView.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically; } - (BOOL)enablesReturnKeyAutomatically { return internalTextView.enablesReturnKeyAutomatically; } /////////////////////////////////////////////////////////////////////////////////////////////////// -(void)setDataDetectorTypes:(UIDataDetectorTypes)datadetector { internalTextView.dataDetectorTypes = datadetector; } -(UIDataDetectorTypes)dataDetectorTypes { return internalTextView.dataDetectorTypes; } /////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)hasText{ return [internalTextView hasText]; } - (void)scrollRangeToVisible:(NSRange)range { [internalTextView scrollRangeToVisible:range]; } ///////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - #pragma mark UITextViewDelegate /////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)textViewShouldBeginEditing:(UITextView *)textView { if ([delegate respondsToSelector:@selector(growingTextViewShouldBeginEditing:)]) { return [delegate growingTextViewShouldBeginEditing:self]; } else { return YES; } } /////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)textViewShouldEndEditing:(UITextView *)textView { if ([delegate respondsToSelector:@selector(growingTextViewShouldEndEditing:)]) { return [delegate growingTextViewShouldEndEditing:self]; } else { return YES; } } /////////////////////////////////////////////////////////////////////////////////////////////////// - (void)textViewDidBeginEditing:(UITextView *)textView { if ([delegate respondsToSelector:@selector(growingTextViewDidBeginEditing:)]) { [delegate growingTextViewDidBeginEditing:self]; } } /////////////////////////////////////////////////////////////////////////////////////////////////// - (void)textViewDidEndEditing:(UITextView *)textView { if ([delegate respondsToSelector:@selector(growingTextViewDidEndEditing:)]) { [delegate growingTextViewDidEndEditing:self]; } } /////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)atext { //weird 1 pixel bug when clicking backspace when textView is empty if(![textView hasText] && [atext isEqualToString:@""]) return NO; //Added by bretdabaker: sometimes we want to handle this ourselves if ([delegate respondsToSelector:@selector(growingTextView:shouldChangeTextInRange:replacementText:)]) return [delegate growingTextView:self shouldChangeTextInRange:range replacementText:atext]; if ([atext isEqualToString:@"\n"]) { if ([delegate respondsToSelector:@selector(growingTextViewShouldReturn:)]) { if (![delegate performSelector:@selector(growingTextViewShouldReturn:) withObject:self]) { return YES; } else { [textView resignFirstResponder]; return NO; } } } return YES; } /////////////////////////////////////////////////////////////////////////////////////////////////// - (void)textViewDidChangeSelection:(UITextView *)textView { if ([delegate respondsToSelector:@selector(growingTextViewDidChangeSelection:)]) { [delegate growingTextViewDidChangeSelection:self]; } } @end