UITextView實(shí)現(xiàn)placeHold提示、根據(jù)輸入內(nèi)容動(dòng)態(tài)調(diào)整高度以及可插入特殊文本

項(xiàng)目中遇到的需求,需要自定義UITextView,實(shí)現(xiàn)以下功能:

  • 添加placeHold提示,類似UITextField的placeholder默認(rèn)提示,并根據(jù)輸入文字自動(dòng)提示;
  • UITextView高度可根據(jù)輸入內(nèi)容動(dòng)態(tài)調(diào)整,當(dāng)超出maxHeight時(shí),高度不再增加;
  • 輸入時(shí)可插入不可編輯的自定義文本(如#主題#,@人名),類似微信輸入時(shí)候的@人名,插入的文本要有不同顏色顯示,并且插入文本不可編輯,刪除時(shí)候則統(tǒng)一刪除。

簡(jiǎn)單效果如圖所示

效果圖

實(shí)現(xiàn)細(xì)節(jié)

自定義CJUITextView繼承自UITextView,并且自身實(shí)現(xiàn)了UITextViewDelegate的代理,同時(shí)新增CJUITextViewDelegate代理方法:

@protocol CJUITextViewDelegate <NSObject>
@optional
/**
*  CJUITextView輸入了done的回調(diào)
*  一般在self.textView.returnKeyType = UIReturnKeyDone;時(shí)執(zhí)行該回調(diào)
*
*  @param textView
*
*  @return
*/
- (void)CJUITextViewEnterDone:(CJUITextView *)textView;

/**
 *  CJUITextView自動(dòng)改變高度
 *
 *  @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

從方法命名可以看出,除了前兩個(gè)方法,后面的代理方法都是跟UITextViewDelegate的代理一樣的。

1. placeHold提示

添加UILabel(placeHoldLabel)到UITextView上,設(shè)置默認(rèn)字體、顏色等屬性,然后在drawRect:方法中調(diào)整其frame值大小。同時(shí)在textViewDidChangeSelection:代理回調(diào)中判斷placeHoldLabel的hidden值,并根據(jù)當(dāng)前UITextView高度,調(diào)整placeHoldLabel的大小。

與placeHold提示相關(guān)的屬性:

 @property (nonatomic, copy, setter=setPlaceHoldString:)   NSString *placeHoldString;
 @property (nonatomic, strong, setter=setPlaceHoldTextFont:) UIFont *placeHoldTextFont;
 @property (nonatomic, strong, setter=setPlaceHoldTextColor:) UIColor *placeHoldTextColor;
2. UITextView高度動(dòng)態(tài)調(diào)整

設(shè)置屬性

/**
 *  是否根據(jù)輸入內(nèi)容自動(dòng)調(diào)整高度(default NO)
 */
@property (nonatomic, assign, setter=setAutoLayoutHeight:) BOOL autoLayoutHeight;
/**
 *  autoLayoutHeight為YES時(shí)的最大高度(default MAXFLOAT)
 */
@property (nonatomic, assign) CGFloat maxHeight;

有個(gè)注意點(diǎn),當(dāng)開(kāi)啟autoLayoutHeight后,實(shí)現(xiàn)中默認(rèn)將輸入的自動(dòng)聯(lián)想功能關(guān)閉了self.autocorrectionType = UITextAutocorrectionTypeNo;因?yàn)橛脩羧绻沁x擇輸入了聯(lián)想關(guān)鍵詞時(shí),UITextView在動(dòng)態(tài)調(diào)整高度的同時(shí)會(huì)出現(xiàn)輸入光標(biāo)跳動(dòng)的問(wèn)題,這里暫時(shí)只能如此處理。
在textViewDidChangeSelection:回調(diào)中執(zhí)行[self changeSize];動(dòng)態(tài)調(diào)整高度:
- (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)];是為了保證調(diào)整高度后,光標(biāo)始終在輸入文本的后面。

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


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

調(diào)用示例:

//插入文本的顏色
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設(shè)置所有插入文本的顏色,當(dāng)然也可以在-insterSpecialTextAndGetSelectedRange: selectedRange: text:方法中單獨(dú)對(duì)插入文本進(jìn)行設(shè)置。
-insterSpecialTextAndGetSelectedRange: selectedRange: text:方法的實(shí)現(xiàn)邏輯如下:

  • 首先讀取插入文本的Attribute屬性,并設(shè)置字體大小與文本顏色,同時(shí)增加一個(gè)自定義屬性SPECIAL_TEXT_NUM(用來(lái)區(qū)分插入文本的不同顏色顯示,以及刪除插入文本)

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

self.specialTextNum初始值為1,并根據(jù)插入特殊文本的次數(shù)自增長(zhǎng)。

  • 判斷插入文本的位置selectedRange,根據(jù)插入位置將插入前的文本做截取,最后和插入文本拼接在一起。
4. 刪除自定義文本,則需要同時(shí)刪除

-textView: shouldChangeTextInRange: replacementText:回調(diào)中判斷,通過(guò)枚舉NSAttributedString的自定義屬性SPECIAL_TEXT_NUM,如果判斷到需要?jiǎng)h除的文本中包含SPECIAL_TEXT_NUM屬性,則將所有SPECIAL_TEXT_NUM的值相等的文本都刪除,因?yàn)樵趇nsterSpecialText的時(shí)候已經(jīng)保證了插入文本的每個(gè)字符都會(huì)有相同的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. 保證插入文本不可編輯

這里不可編輯的意思是:文本插入后不能刪減自定義文本的文字或者在中間增加文字。
那么只需要保證光標(biāo)不能移動(dòng)到插入文本之中就可以了。
借助runtime,發(fā)現(xiàn)光標(biāo)移動(dòng)的時(shí)候會(huì)觸發(fā)selectedTextRange屬性的改變,這就好辦了,KVO注冊(cè)對(duì)selectedTextRange屬性的監(jiān)測(cè),在KVO的監(jiān)測(cè)方法中對(duì)光標(biāo)位置進(jìn)行處理
- (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) {
            //判斷光標(biāo)移動(dòng),光標(biāo)不能處在特殊文本內(nèi)
            [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)) {
                    //光標(biāo)距離左邊界的值
                    NSUInteger leftValue = newRange.location - range.location;
                    //光標(biāo)距離右邊界的值
                    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;這是用來(lái)重設(shè)UITextView的默認(rèn)輸入屬性的。因?yàn)楫?dāng)你插入了一段不同顏色的特殊文本,或者將光標(biāo)移動(dòng)到了特殊文本的后面,繼續(xù)輸入時(shí)文字的顏色會(huì)跟特殊文本的顏色一樣,很明顯這不是我們想要的。所以有好幾個(gè)地方需要重設(shè)typingAttributes的值,分別在:-textViewDidChangeSelection: -textView: shouldChangeTextInRange:replacementText: -observeValueForKeyPath:ofObject:change:context:這三個(gè)方法中。而輸入的默認(rèn)屬性self.defaultAttributes是由懶加載實(shí)現(xiàn)的:
- (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,另外該項(xiàng)目已集成cocopads依賴,引用方法:

platform :ios, '8.0'
pod 'CJTextView', '~> 1.0.1'





歡迎支持本人博客,歡迎star GitHub源碼。。。不要臉的打廣告????
有問(wèn)題請(qǐng)留言~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容