项目中遇到的需求,需要自定义UITextView,实现以下功能:
- 添加placeHold提示,类似UITextField的placeholder默认提示,并根据输入文字自动提示;
- UITextView高度可根据输入内容动态调整,当超出maxHeight时,高度不再增加;
- 输入时可插入不可编辑的自定义文本(如 #主题# , @人名 ),类似微信输入时候的@人名,插入的文本要有不同颜色显示,并且插入文本不可编辑,删除时候则统一删除。
简单效果如图所示

实现细节
自定义CJUITextView继承自UITextView,并且自身实现了UITextViewDelegate的代理,同时新增CJUITextViewDelegate代理方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @protocol CJUITextViewDelegate <NSObject> @optional
- (void)CJUITextViewEnterDone:(CJUITextView *)textView;
- (void)CJUITextView:(CJUITextView *)textView heightChanged:(CGRect)frame;
- (BOOL)textViewShouldBeginEditing:(CJUITextView *)textView; - (BOOL)textViewShouldEndEditing:(CJUITextView *)textView;
- (void)textViewDidBeginEditing:(CJUITextView *)textView; - (void)textViewDidEndEditing:(CJUITextView *)textView;
- (BOOL)textView:(CJUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text; - (void)textViewDidChange:(CJUITextView *)textView;
- (void)textViewDidChangeSelection:(CJUITextView *)textView;
- (BOOL)textView:(CJUITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange NS_AVAILABLE_IOS(7_0); - (BOOL)textView:(CJUITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange NS_AVAILABLE_IOS(7_0);
@end
|
从方法命名可以看出,除了前两个方法,后面的代理方法都是跟UITextViewDelegate的代理一样的。
placeHold提示
添加UILabel(placeHoldLabel)到UITextView上,设置默认字体、颜色等属性,然后在drawRect:方法中调整其frame值大小。同时在textViewDidChangeSelection:代理回调中判断placeHoldLabel的hidden值,并根据当前UITextView高度,调整placeHoldLabel的大小。
与placeHold提示相关的属性:
1 2 3
| @property (nonatomic, copy, setter=setPlaceHoldString:) NSString *placeHoldString; @property (nonatomic, strong, setter=setPlaceHoldTextFont:) UIFont *placeHoldTextFont; @property (nonatomic, strong, setter=setPlaceHoldTextColor:) UIColor *placeHoldTextColor;
|
UITextView高度动态调整
设置属性
1 2 3 4 5 6 7 8
|
@property (nonatomic, assign, setter=setAutoLayoutHeight:) BOOL autoLayoutHeight;
@property (nonatomic, assign) CGFloat maxHeight;
|
有个注意点,当开启autoLayoutHeight后,实现中默认将输入的自动联想功能关闭了self.autocorrectionType = UITextAutocorrectionTypeNo;因为用户如果是选择输入了联想关键词时,UITextView在动态调整高度的同时会出现输入光标跳动的问题,这里暂时只能如此处理。
在textViewDidChangeSelection:回调中执行[self changeSize];动态调整高度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| - (void)changeSize { CGRect oriFrame = self.frame; CGSize sizeToFit = [self sizeThatFits:CGSizeMake(oriFrame.size.width, MAXFLOAT)]; if (sizeToFit.height < self.defaultFrame.size.height) { sizeToFit.height = self.defaultFrame.size.height; } if (oriFrame.size.height != sizeToFit.height && sizeToFit.height <= self.maxHeight) { oriFrame.size.height = sizeToFit.height; self.frame = oriFrame; if (self.myDelegate && [self.myDelegate respondsToSelector:@selector(CJUITextView:heightChanged:)]) { [self.myDelegate CJUITextView:self heightChanged:oriFrame]; } } [self scrollRangeToVisible:NSMakeRange(self.text.length, 0)]; }
|
最后一行代码[self scrollRangeToVisible:NSMakeRange(self.text.length, 0)];是为了保证调整高度后,光标始终在输入文本的后面。
插入自定义文本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@property (nonatomic, strong, getter=getSpecialTextColor) UIColor *specialTextColor;
- (NSRange)insterSpecialTextAndGetSelectedRange:(NSAttributedString *)specialText selectedRange:(NSRange)selectedRange text:(NSAttributedString *)attributedText;
|
调用示例:
1 2 3 4 5
| self.textView.specialTextColor = [UIColor redColor]; NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:@"#插入文本#"]; [str addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:16] range:NSMakeRange(0, str.length)]; [self.textView insterSpecialTextAndGetSelectedRange:str selectedRange:self.textView.selectedRange text:self.textView.attributedText];
|
self. specialTextColor设置所有插入文本的颜色,当然也可以在-insterSpecialTextAndGetSelectedRange: selectedRange: text:方法中单独对插入文本进行设置。
-insterSpecialTextAndGetSelectedRange: selectedRange: text:方法的实现逻辑如下:
- 首先读取插入文本的Attribute属性,并设置字体大小与文本颜色,同时增加一个自定义属性
SPECIAL_TEXT_NUM(用来区分插入文本的不同颜色显示,以及删除插入文本)1 2 3
| self.specialTextNum ++; [specialTextAttStr addAttribute:SPECIAL_TEXT_NUM value:@(self.specialTextNum) range:specialRange];
|
self.specialTextNum初始值为1,并根据插入特殊文本的次数自增长。
- 判断插入文本的位置
selectedRange,根据插入位置将插入前的文本做截取,最后和插入文本拼接在一起。
删除自定义文本,则需要同时删除
在-textView: shouldChangeTextInRange: replacementText:回调中判断,通过枚举NSAttributedString的自定义属性SPECIAL_TEXT_NUM,如果判断到需要删除的文本中包含SPECIAL_TEXT_NUM属性,则将所有SPECIAL_TEXT_NUM的值相等的文本都删除,因为在insterSpecialText的时候已经保证了插入文本的每个字符都会有相同的SPECIAL_TEXT_NUM属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if ([text isEqualToString:@""]) { __block BOOL deleteSpecial = NO; NSRange oldRange = textView.selectedRange; [textView.attributedText enumerateAttribute:SPECIAL_TEXT_NUM inRange:NSMakeRange(0, textView.selectedRange.location) options:NSAttributedStringEnumerationReverse usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { NSRange deleteRange = NSMakeRange(textView.selectedRange.location-1, 0) ; if (attrs != nil && attrs != 0) { if (deleteRange.location > range.location && deleteRange.location < (range.location+range.length)) { NSMutableAttributedString *textAttStr = [[NSMutableAttributedString alloc] initWithAttributedString:textView.attributedText]; [textAttStr deleteCharactersInRange:range]; textView.attributedText = textAttStr; deleteSpecial = YES; textView.selectedRange = NSMakeRange(oldRange.location-range.length, 0); *stop = YES; } } }]; return !deleteSpecial; }
|
保证插入文本不可编辑
这里不可编辑的意思是:文本插入后不能删减自定义文本的文字或者在中间增加文字。
那么只需要保证光标不能移动到插入文本之中就可以了。
借助runtime,发现光标移动的时候会触发selectedTextRange属性的改变,这就好办了,KVO注册对selectedTextRange属性的监测,在KVO的监测方法中对光标位置进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| - (void)observeValueForKeyPath:(NSString*) path ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if (context == TextViewObserverSelectedTextRange && [path isEqual:@"selectedTextRange"]){ UITextRange *newContentStr = [change objectForKey:@"new"]; UITextRange *oldContentStr = [change objectForKey:@"old"]; NSRange newRange = [self selectedRange:self selectTextRange:newContentStr]; NSRange oldRange = [self selectedRange:self selectTextRange:oldContentStr]; if (newRange.location != oldRange.location) { [self.attributedText enumerateAttribute:SPECIAL_TEXT_NUM inRange:NSMakeRange(0, self.attributedText.length) options:NSAttributedStringEnumerationReverse usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { if (attrs != nil && attrs != 0) { if (newRange.location > range.location && newRange.location < (range.location+range.length)) { NSUInteger leftValue = newRange.location - range.location; NSUInteger rightValue = range.location+range.length - newRange.location; if (leftValue >= rightValue) { self.selectedRange = NSMakeRange(self.selectedRange.location-leftValue, 0); }else{ self.selectedRange = NSMakeRange(self.selectedRange.location+rightValue, 0); } } } }]; } } self.typingAttributes = self.defaultAttributes; }
|
可能你注意到了最后一行代码self.typingAttributes = self.defaultAttributes;这是用来重设UITextView的默认输入属性的。因为当你插入了一段不同颜色的特殊文本,或者将光标移动到了特殊文本的后面,继续输入时文字的颜色会跟特殊文本的颜色一样,很明显这不是我们想要的。所以有好几个地方需要重设typingAttributes的值,分别在:
1 2 3
| -textViewDidChangeSelection: -textView:shouldChangeTextInRange:replacementText: -observeValueForKeyPath:ofObject:change:context:
|
这三个方法中。而输入的默认属性self.defaultAttributes是由懒加载实现的:
1 2 3 4 5 6 7 8 9 10 11
| - (NSMutableDictionary *)defaultAttributes { if (!_defaultAttributes) { _defaultAttributes = [NSMutableDictionary dictionary]; [_defaultAttributes setObject:self.font forKey:NSFontAttributeName]; if (!self.textColor || self.textColor == nil) { self.textColor = [UIColor blackColor]; } [_defaultAttributes setObject:self.textColor forKey:NSForegroundColorAttributeName]; } return _defaultAttributes; }
|
最后
最后奉上GitHub源码地址TextViewDemo,另外该项目已集成cocopads依赖,引用方法:
platform :ios, ‘8.0’
pod ‘CJTextView’, ‘~> 1.0.1’
欢迎支持本人博客,欢迎star。。。不要脸的打广告😁😂有问题请留言
本人GitHub