diff --git a/HPGrowingTextView.h b/HPGrowingTextView.h index 8311a41..cab56ab 100644 --- a/HPGrowingTextView.h +++ b/HPGrowingTextView.h @@ -27,6 +27,12 @@ #import +#if __IPHONE_OS_VERSION_MAX_ALLOWED < 60000 + // UITextAlignment is deprecated in iOS 6.0+, use NSTextAlignment instead. + // Reference: https://developer.apple.com/library/ios/documentation/uikit/reference/NSString_UIKit_Additions/Reference/Reference.html + #define NSTextAlignment UITextAlignment +#endif + @class HPGrowingTextView; @class HPTextViewInternal; @@ -60,17 +66,16 @@ int minNumberOfLines; BOOL animateHeightChange; + NSTimeInterval animationDuration; //uitextview properties - NSObject *delegate; - NSString *text; - UIFont *font; - UIColor *textColor; - UITextAlignment textAlignment; + NSObject *__unsafe_unretained delegate; + NSTextAlignment textAlignment; NSRange selectedRange; BOOL editable; UIDataDetectorTypes dataDetectorTypes; UIReturnKeyType returnKeyType; + UIKeyboardType keyboardType; UIEdgeInsets contentInset; } @@ -78,28 +83,45 @@ //real class properties @property int maxNumberOfLines; @property int minNumberOfLines; +@property (nonatomic) int maxHeight; +@property (nonatomic) int minHeight; @property BOOL animateHeightChange; -@property (retain) UITextView *internalTextView; +@property NSTimeInterval animationDuration; +@property (nonatomic, strong) NSString *placeholder; +@property (nonatomic, strong) NSAttributedString *attributedPlaceholder; +@property (nonatomic, strong) UIColor *placeholderColor; +@property (nonatomic, strong) UITextView *internalTextView; //uitextview properties -@property(assign) NSObject *delegate; -@property(nonatomic,assign) NSString *text; -@property(nonatomic,assign) UIFont *font; -@property(nonatomic,assign) UIColor *textColor; -@property(nonatomic) UITextAlignment textAlignment; // default is UITextAlignmentLeft +@property(unsafe_unretained) NSObject *delegate; +@property(nonatomic,strong) NSString *text; +@property(nonatomic,strong) UIFont *font; +@property(nonatomic,strong) UIColor *textColor; +@property(nonatomic) NSTextAlignment textAlignment; // default is NSTextAlignmentLeft @property(nonatomic) NSRange selectedRange; // only ranges of length 0 are supported @property(nonatomic,getter=isEditable) BOOL editable; @property(nonatomic) UIDataDetectorTypes dataDetectorTypes __OSX_AVAILABLE_STARTING(__MAC_NA, __IPHONE_3_0); @property (nonatomic) UIReturnKeyType returnKeyType; +@property (nonatomic) UIKeyboardType keyboardType; @property (assign) UIEdgeInsets contentInset; +@property (nonatomic) BOOL isScrollable; +@property(nonatomic) BOOL enablesReturnKeyAutomatically; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 +- (id)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer; +#endif //uitextview methods //need others? use .internalTextView - (BOOL)becomeFirstResponder; - (BOOL)resignFirstResponder; +- (BOOL)isFirstResponder; - (BOOL)hasText; - (void)scrollRangeToVisible:(NSRange)range; +// call to force a height change (e.g. after you change max/min lines) +- (void)refreshHeight; + @end diff --git a/HPGrowingTextView.m b/HPGrowingTextView.m index 7537120..d6de4af 100644 --- a/HPGrowingTextView.m +++ b/HPGrowingTextView.m @@ -37,15 +37,19 @@ -(void)growDidStop; @implementation HPGrowingTextView @synthesize internalTextView; @synthesize delegate; - +@synthesize maxHeight; +@synthesize minHeight; @synthesize font; @synthesize textColor; @synthesize textAlignment; @synthesize selectedRange; @synthesize editable; -@synthesize dataDetectorTypes; +@synthesize dataDetectorTypes; @synthesize animateHeightChange; +@synthesize animationDuration; @synthesize returnKeyType; +@dynamic placeholder; +@dynamic placeholderColor; // having initwithcoder allows us to use HPGrowingTextView in a Nib. -- aob, 9/2011 - (id)initWithCoder:(NSCoder *)aDecoder @@ -63,56 +67,73 @@ - (id)initWithFrame:(CGRect)frame { 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]; - UIView *internal = (UIView*)[[internalTextView subviews] objectAtIndex:0]; - minHeight = internal.frame.size.height; + minHeight = internalTextView.frame.size.height; minNumberOfLines = 1; animateHeightChange = YES; + animationDuration = 0.1f; internalTextView.text = @""; [self setMaxNumberOfLines:3]; + + [self setPlaceholderColor:[UIColor lightGrayColor]]; + internalTextView.displayPlaceHolder = YES; } --(void)sizeToFit +-(CGSize)sizeThatFits:(CGSize)size { - 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 - // fix from Ankit Thakur - if ([self.text length] > 0) { - return; - } else { - r.size.height = minHeight; - self.frame = r; + if (self.text.length == 0) { + size.height = minHeight; } + return size; } --(void)setFrame:(CGRect)aframe +-(void)layoutSubviews { - CGRect r = aframe; + [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; - - [super setFrame:aframe]; + internalTextView.frame = r; } -(void)setContentInset:(UIEdgeInsets)inset @@ -137,6 +158,8 @@ -(UIEdgeInsets)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 = @"-"; @@ -148,7 +171,7 @@ -(void)setMaxNumberOfLines:(int)n internalTextView.text = newText; - maxHeight = internalTextView.contentSize.height; + maxHeight = [self measureHeight]; internalTextView.text = saveText; internalTextView.hidden = NO; @@ -164,8 +187,16 @@ -(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 = @"-"; @@ -177,7 +208,7 @@ -(void)setMinNumberOfLines:(int)m internalTextView.text = newText; - minHeight = internalTextView.contentSize.height; + minHeight = [self measureHeight]; internalTextView.text = saveText; internalTextView.hidden = NO; @@ -193,30 +224,81 @@ -(int)minNumberOfLines return minNumberOfLines; } +- (void)setMinHeight:(int)height +{ + minHeight = height; + minNumberOfLines = 0; +} + +- (NSString *)placeholder +{ + return internalTextView.placeholder; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + [internalTextView setPlaceholder:placeholder]; + [internalTextView setNeedsDisplay]; +} + +- (void)setAttributedPlaceholder:(NSAttributedString *)attributedPlaceholder +{ + [internalTextView setPlaceholder:nil]; + [internalTextView setAttributedPlaceholder:attributedPlaceholder]; + [internalTextView setNeedsDisplay]; +} + +- (UIColor *)placeholderColor +{ + return internalTextView.placeholderColor; +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + [internalTextView setPlaceholderColor:placeholderColor]; +} - (void)textViewDidChange:(UITextView *)textView -{ +{ + [self refreshHeight]; +} + +- (void)refreshHeight +{ //size of content, so we can set the frame of self - NSInteger newSizeH = internalTextView.contentSize.height; - if(newSizeH < minHeight || !internalTextView.hasText) newSizeH = minHeight; //not smalles than minHeight + 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) { - // [fixed] Pasting too much text into the view failed to fire the height change, - // thanks to Gwynne - - if (newSizeH > maxHeight && internalTextView.frame.size.height <= maxHeight) + // if our new height is greater than the maxHeight + // sets not set the height or move things + // around and enable scrolling + if (newSizeH >= maxHeight) { - 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:0.1f + [UIView animateWithDuration:animationDuration delay:0 options:(UIViewAnimationOptionAllowUserInteraction| UIViewAnimationOptionBeginFromCurrentState) @@ -231,7 +313,7 @@ - (void)textViewDidChange:(UITextView *)textView #endif } else { [UIView beginAnimations:@"" context:nil]; - [UIView setAnimationDuration:0.1f]; + [UIView setAnimationDuration:animationDuration]; [UIView setAnimationDelegate:self]; [UIView setAnimationDidStopSelector:@selector(growDidStop)]; [UIView setAnimationBeginsFromCurrentState:YES]; @@ -248,29 +330,47 @@ - (void)textViewDidChange:(UITextView *)textView } } } - - - // 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; - } - } + // Display (or not) the placeholder string + + BOOL wasDisplayingPlaceholder = internalTextView.displayPlaceHolder; + internalTextView.displayPlaceHolder = self.internalTextView.text.length == 0; - - if ([delegate respondsToSelector:@selector(growingTextViewDidChange:)]) { + 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 @@ -285,17 +385,21 @@ -(void)resizeTextView:(NSInteger)newSizeH internalTextViewFrame.origin.y = contentInset.top - contentInset.bottom; internalTextViewFrame.origin.x = contentInset.left; - internalTextViewFrame.size.width = internalTextView.contentSize.width; - internalTextView.frame = internalTextViewFrame; + if(!CGRectEqualToRect(internalTextView.frame, internalTextViewFrame)) internalTextView.frame = internalTextViewFrame; } --(void)growDidStop +- (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 @@ -315,12 +419,13 @@ -(BOOL)resignFirstResponder return [internalTextView resignFirstResponder]; } -- (void)dealloc { - [internalTextView release]; - [super dealloc]; +-(BOOL)isFirstResponder +{ + return [self.internalTextView isFirstResponder]; } + /////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark UITextView properties /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -367,12 +472,25 @@ -(UIColor*)textColor{ /////////////////////////////////////////////////////////////////////////////////////////////////// --(void)setTextAlignment:(UITextAlignment)aligment +-(void)setBackgroundColor:(UIColor *)backgroundColor +{ + [super setBackgroundColor:backgroundColor]; + internalTextView.backgroundColor = backgroundColor; +} + +-(UIColor*)backgroundColor +{ + return internalTextView.backgroundColor; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +-(void)setTextAlignment:(NSTextAlignment)aligment { internalTextView.textAlignment = aligment; } --(UITextAlignment)textAlignment +-(NSTextAlignment)textAlignment { return internalTextView.textAlignment; } @@ -391,6 +509,18 @@ -(NSRange)selectedRange /////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setIsScrollable:(BOOL)isScrollable +{ + internalTextView.scrollEnabled = isScrollable; +} + +- (BOOL)isScrollable +{ + return internalTextView.scrollEnabled; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + -(void)setEditable:(BOOL)beditable { internalTextView.editable = beditable; @@ -415,6 +545,30 @@ -(UIReturnKeyType)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; @@ -487,6 +641,10 @@ - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range //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]) { diff --git a/HPTextViewInternal.h b/HPTextViewInternal.h index c44ac63..35cc4a6 100644 --- a/HPTextViewInternal.h +++ b/HPTextViewInternal.h @@ -28,7 +28,11 @@ #import -@interface HPTextViewInternal : UITextView { -} +@interface HPTextViewInternal : UITextView + +@property (nonatomic, strong) NSString *placeholder; +@property (nonatomic, strong) NSAttributedString *attributedPlaceholder; +@property (nonatomic, strong) UIColor *placeholderColor; +@property (nonatomic) BOOL displayPlaceHolder; @end diff --git a/HPTextViewInternal.m b/HPTextViewInternal.m index b1257be..bdcab27 100644 --- a/HPTextViewInternal.m +++ b/HPTextViewInternal.m @@ -30,6 +30,22 @@ @implementation HPTextViewInternal +-(void)setText:(NSString *)text +{ + BOOL originalValue = self.scrollEnabled; + //If one of GrowingTextView's superviews is a scrollView, and self.scrollEnabled == NO, + //setting the text programatically will cause UIKit to search upwards until it finds a scrollView with scrollEnabled==yes + //then scroll it erratically. Setting scrollEnabled temporarily to YES prevents this. + [self setScrollEnabled:YES]; + [super setText:text]; + [self setScrollEnabled:originalValue]; +} + +- (void)setScrollable:(BOOL)isScrollable +{ + [super setScrollEnabled:isScrollable]; +} + -(void)setContentOffset:(CGPoint)s { if(self.tracking || self.decelerating){ @@ -50,7 +66,11 @@ -(void)setContentOffset:(CGPoint)s self.contentInset = insets; } } - + + // Fix "overscrolling" bug + if (s.y > self.contentSize.height - self.frame.size.height && !self.decelerating && !self.tracking && !self.dragging) + s = CGPointMake(s.x, self.contentSize.height - self.frame.size.height); + [super setContentOffset:s]; } @@ -78,10 +98,36 @@ -(void)setContentSize:(CGSize)contentSize [super setContentSize:contentSize]; } - -- (void)dealloc { - [super dealloc]; +- (void)drawRect:(CGRect)rect +{ + [super drawRect:rect]; + if (self.displayPlaceHolder && self.placeholder && self.placeholderColor) + { + if ([self respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)]) + { + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = self.textAlignment; + [self.placeholder drawInRect:CGRectMake(5, 8 + self.contentInset.top, self.frame.size.width-self.contentInset.left, self.frame.size.height- self.contentInset.top) withAttributes:@{NSFontAttributeName:self.font, NSForegroundColorAttributeName:self.placeholderColor, NSParagraphStyleAttributeName:paragraphStyle}]; + } + else { + [self.placeholderColor set]; + [self.placeholder drawInRect:CGRectMake(8.0f, 8.0f, self.frame.size.width - 16.0f, self.frame.size.height - 16.0f) withFont:self.font]; + } + } else if (self.displayPlaceHolder && self.attributedPlaceholder) { + + [self.attributedPlaceholder drawInRect:CGRectMake(8.0f, 8.0f, self.frame.size.width - 16.0f, self.frame.size.height - 16.0f)]; + } } - +-(void)setPlaceholder:(NSString *)placeholder +{ + _placeholder = placeholder; + + [self setNeedsDisplay]; +} +- (void)setAttributedPlaceholder:(NSAttributedString *)attributedPlaceholder +{ + _attributedPlaceholder = attributedPlaceholder; + [self setNeedsDisplay]; +} @end