UITextView实现placeHold提示、根据输入内容动态调整高度以及可插入特殊文本

项目中遇到的需求,需要自定义UITextView,实现以下功能:

  • 添加placeHold提示,类似UITextField的placeholder默认提示,并根据输入文字自动提示;
  • UITextView高度可根据输入内容动态调整,当超出maxHeight时,高度不再增加;
  • 输入时可插入不可编辑的自定义文本(如#主题#,@人名),类似微信输入时候的@人名,插入的文本要有不同颜色显示,并且插入文本不可编辑,删除时候则统一删除。

简单效果如图所示

效果图

实现细节

自定义CJUITextView继承自UITextView,并且自身实现了UITextViewDelegate的代理,同时新增CJUITextViewDelegate代理方法:

@protocol CJUITextViewDelegate <NSObject>
@optional
/**
*  CJUITextView输入了done的回调
*  一般在self.textView.returnKeyType = UIReturnKeyDone;时执行该回调
*
*  @param textView
*
*  @return
*/
- (void)CJUITextViewEnterDone:(CJUITextView *)textView;

/**
 *  CJUITextView自动改变高度
 *
 *  @param textView
 *  @param frame     改变高度后的size
 */
 - (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的代理一样的。

1. placeHold提示

添加UILabel(placeHoldLabel)到UITextView上,设置默认字体、颜色等属性,然后在drawRect:方法中调整其frame值大小。同时在textViewDidChangeSelection:代理回调中判断placeHoldLabel的hidden值,并根据当前UITextView高度,调整placeHoldLabel的大小。

与placeHold提示相关的属性:

 @property (nonatomic, copy, setter=setPlaceHoldString:)   NSString *placeHoldString;
 @property (nonatomic, strong, setter=setPlaceHoldTextFont:) UIFont *placeHoldTextFont;
 @property (nonatomic, strong, setter=setPlaceHoldTextColor:) UIColor *placeHoldTextColor;
2. UITextView高度动态调整

设置属性

/**
 *  是否根据输入内容自动调整高度(default NO)
 */
@property (nonatomic, assign, setter=setAutoLayoutHeight:) BOOL autoLayoutHeight;
/**
 *  autoLayoutHeight为YES时的最大高度(default MAXFLOAT)
 */
@property (nonatomic, assign) CGFloat maxHeight;

有个注意点,当开启autoLayoutHeight后,实现中默认将输入的自动联想功能关闭了self.autocorrectionType = UITextAutocorrectionTypeNo;因为用户如果是选择输入了联想关键词时,UITextView在动态调整高度的同时会出现输入光标跳动的问题,这里暂时只能如此处理。
在textViewDidChangeSelection:回调中执行[self changeSize];动态调整高度:
- (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)];是为了保证调整高度后,光标始终在输入文本的后面。

3. 插入自定义文本
/**
 *  插入文本的颜色(default self.textColor)
 */
@property (nonatomic, strong, getter=getSpecialTextColor) UIColor *specialTextColor;


/**
 *  在指定位置插入字符,并返回插入字符后的SelectedRange值
 *
 *  @param specialText    要插入的字符
 *  @param selectedRange  插入位置
 *  @param attributedText 插入前的文本
 *
 *  @return 插入字符后的光标位置
 */
- (NSRange)insterSpecialTextAndGetSelectedRange:(NSAttributedString *)specialText
                              selectedRange:(NSRange)selectedRange
                                       text:(NSAttributedString *)attributedText;

调用示例:

//插入文本的颜色
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(用来区分插入文本的不同颜色显示,以及删除插入文本)

    //为插入文本增加SPECIAL_TEXT_NUM索引
    self.specialTextNum ++;
    [specialTextAttStr addAttribute:SPECIAL_TEXT_NUM value:@(self.specialTextNum) range:specialRange];
    

self.specialTextNum初始值为1,并根据插入特殊文本的次数自增长。

  • 判断插入文本的位置selectedRange,根据插入位置将插入前的文本做截取,最后和插入文本拼接在一起。
4. 删除自定义文本,则需要同时删除

-textView: shouldChangeTextInRange: replacementText:回调中判断,通过枚举NSAttributedString的自定义属性SPECIAL_TEXT_NUM,如果判断到需要删除的文本中包含SPECIAL_TEXT_NUM属性,则将所有SPECIAL_TEXT_NUM的值相等的文本都删除,因为在insterSpecialText的时候已经保证了插入文本的每个字符都会有相同的SPECIAL_TEXT_NUM属性。

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;
}
5. 保证插入文本不可编辑

这里不可编辑的意思是:文本插入后不能删减自定义文本的文字或者在中间增加文字。
那么只需要保证光标不能移动到插入文本之中就可以了。
借助runtime,发现光标移动的时候会触发selectedTextRange属性的改变,这就好办了,KVO注册对selectedTextRange属性的监测,在KVO的监测方法中对光标位置进行处理
- (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的值,分别在:-textViewDidChangeSelection: -textView: shouldChangeTextInRange:replacementText: -observeValueForKeyPath:ofObject:change:context:这三个方法中。而输入的默认属性self.defaultAttributes是由懒加载实现的:
- (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源码。。。不要脸的打广告😁😂
有问题请留言~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容