限制最大文字个数-兼容选中、粘贴、联想

2021-08-05  本文已影响0人  山已几孑

大家肯定都写过UITextView/UITextField限制文字个数的需求,网上的说明也有大把,但是效果千奇百怪, 对中文的适配也是各显神通,这里就再加一个我自己的做法。

需求是限制字数,超出后无法输入,中文输入超出后截断

写在前面

这个问题写了好多年,这次被抓住了,只能优化了,翻看了网上的一些做法

-(void)textViewDidChange:(UITextView*)textView
{
    NSString*textString = textView.text;
    NSString*language = textView.textInputMode.primaryLanguage;
    //中文输入
    if([language isEqualToString:@"zh-Hans"]) {
        UITextRange*selectedRange = [textView markedTextRange];
        if(!selectedRange) {
            if(textString.length > 1000) {
                self.talkAboutView.textView.text = [textString substringToIndex:1000];
                alert(@"最多可输入1000字");
            }
        }else{}
    }else{
        if(textString.length > 1000) {
            self.talkAboutView.textView.text= [textString substringToIndex:1000];
        }
    }
}

其实这种办法基本已经满足了需要,但是这里存在一个问题,就是,当你的光标不是在最后面的时候,文字的插入是在中间,然而这里进行的截断却是在最后面,导致中间部分可以继续输入,尾部被一点一点的顶出去。

因此,我对这种方法进行了一些改良

中级-改

首先,我还是把判断的位置,放回到了\- (**BOOL**)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text 中,因为这里面提供了range,markedTextRange,selectedTextRange,可以充分发挥主观能动性,操作text。

这里说下markedTextRange是当前textView的text中,正在被联想的部分的range,可为空selectedTextRange是当前textView中被选中部分的range,不为空,没有选中也会有位置。二者的length不同时不为0,有联想就没选中,因此判断markedTextRange是否存在,作为一个筛选条件。

关于长度判断:

这里也是存在一个弯

//伪代码
textView.text = [oldText] + markedText + [oldText]
or:
textView.text = [oldText] + selectedText + [oldText]

//编辑后的长度 = text长度-(选中or联想)长度 + newText长度
finalLength = textView.text.length - (markedText?markedText.length:selectedText.length) + newText.length

关于插入的位置:

插入位置,就是当markedTextRange存在,那肯定是在markedTextRange.start, 如果不存在,那么就是在selectedTextRange.start; 代码如下:
//需要被替换的长度,markedTextRange的长度,或者selectedTextRange的长度
NSInteger lengthNeedReplace = 0;
// 缓存当前正在操作的position,调光标的时候有用
UITextPosition * replacePosition;
if (markedTextRange) {
            lengthNeedReplace = [target offsetFromPosition:markedTextRange.start toPosition:markedTextRange.end];
            replacePosition = markedTextRange.start;
        } else {
            lengthNeedReplace = [target offsetFromPosition:selectedTextRange.start toPosition:selectedTextRange.end];
            replacePosition = selectedTextRange.start;
        }

关于光标:

上面的方法操作完成后,光标总是会跳到最后面,改进之后,光标跳动距离变少了,跳到了offset为newText.length的位置。但是我们手动修改了newText,并设置了shouldChangeTextInRange 返回NO,因此,需要手动调节光标的位置。关键代码如下:

// 设置光标到正确的位置
              dispatch_async(dispatch_get_main_queue(), ^{
            UITextPosition * p = [target positionFromPosition:replacePosition offset:maxLengthLeft];
            UITextRange * rr = [target textRangeFromPosition:p toPosition:p];
            [target setSelectedTextRange:rr];
        });

全部代码,很少,就不写demo了

这里使用C的方法,因为没有找到办法规避需要使用textView/textField代理的问题,因此使用了公共的方法。

万幸textView/textField 都继承了同一个代理,而且是UITextInput 代理,感兴趣的可以去查看一下里面的方法, 挺多的,都是关于markText\selectedText的,因此下面的方法,使用传入的id<UITextInput>类型,解决了类型的问题。

//*.h
bool ml_shouldChangeTextInRangeWithLimit(id<UITextInput>target,NSInteger limit, NSRange range, NSString* text);
//*.m
bool ml_shouldChangeTextInRangeWithLimit(id<UITextInput>target,NSInteger limit, NSRange range, NSString* text) {
      //哈哈,绕了点,但是没有引入具体类型,知足
    NSString *toBeString = [target textInRange:[target textRangeFromPosition:target.beginningOfDocument toPosition:target.endOfDocument]];

    //text为空的时候,就别管了
    if ([text isEqualToString:@""]) {
        return true;
    }
    NSString *lang = [[UIApplication sharedApplication]textInputMode].primaryLanguage; //ios7之前使用[UITextInputMode currentInputMode].primaryLanguage
    if ([lang isEqualToString:@"zh-Hans"]) { //中文输入
        //选中范围-手动选择的
        UITextRange * selectedTextRange = [target selectedTextRange];
        // 输入-联想输入
        UITextRange *markedTextRange = [target markedTextRange];
        
        NSInteger lengthNeedReplace = 0;
        // 缓存当前正在操作的position,
        UITextPosition * replacePosition;
        
        //之间的关系是:二者的length不同时为0,因此判断markedTextRange是否存在,并设置当前正在操作的position,
        if (markedTextRange) {
            lengthNeedReplace = [target offsetFromPosition:markedTextRange.start toPosition:markedTextRange.end];
            replacePosition = markedTextRange.start;
        } else {
            lengthNeedReplace = [target offsetFromPosition:selectedTextRange.start toPosition:selectedTextRange.end];
            replacePosition = selectedTextRange.start;
        }
        
        NSInteger beforeEditLength = toBeString.length - lengthNeedReplace;
        if (limit - beforeEditLength >= text.length) {
            return true;
        } else {
            //这里就需要替换了
            NSInteger maxLengthLeft = limit - beforeEditLength;
            
            NSString * replaceString = [text substringToIndex:maxLengthLeft];
            // 使用target的协议方法,插入文字, 使textViewDidChange能够被激活
            [target insertText:replaceString];
            
        // 设置光标到正确的位置
        dispatch_async(dispatch_get_main_queue(), ^{
            UITextPosition * p = [target positionFromPosition:replacePosition offset:maxLengthLeft];
            UITextRange * rr = [target textRangeFromPosition:p toPosition:p];
            [target setSelectedTextRange:rr];
        });

            return false;
        }
    } else {//中文输入法以外的直接对其统计限制即可,不考虑其他语种情况
        if (toBeString.length >= limit) {
            return false;
        }
    }
    return true;
};

使用时,还是需要实现textView/textField的代理方法,然后传入我们的方法中

#pragma mark - UITextFieldDelegate methods
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    
    BOOL maxAllowed = ml_shouldChangeTextInRangeWithLimit(textField, 20, range, string);
    // do some other things
    return maxAllowed;
}

//设置textView的placeholder
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    
    BOOL maxAllowed = ml_shouldChangeTextInRangeWithLimit(textView, MAX_VOICE_TEXT_COUNT, range, text);
    // do some other things
    return maxAllowed;
}

PS: 如果上面只限制了中文,如果要限制英文的话,把上面if ([lang isEqualToString:@"zh-Hans"]) { //中文输入的判断去掉就可以了。

bool ml_shouldChangeAllTextInRangeWithLimit(id<UITextInput>target,NSInteger limit, NSRange range, NSString* text) {
    
    NSString *toBeString = [target textInRange:[target textRangeFromPosition:target.beginningOfDocument toPosition:target.endOfDocument]];
    //text为空的时候,就别管了
    if ([text isEqualToString:@""]) {
        return true;
    }
    //选中范围-手动选择的
    UITextRange * selectedTextRange = [target selectedTextRange];
    // 输入-联想输入
    UITextRange *markedTextRange = [target markedTextRange];
    
    NSInteger lengthNeedReplace = 0;
    // 缓存当前正在操作的position,
    UITextPosition * replacePosition;
    
    //之间的关系是:二者的length不同时为0,因此判断markedTextRange是否存在,并设置当前正在操作的position,
    if (markedTextRange) {
        lengthNeedReplace = [target offsetFromPosition:markedTextRange.start toPosition:markedTextRange.end];
        replacePosition = markedTextRange.start;
    } else {
        lengthNeedReplace = [target offsetFromPosition:selectedTextRange.start toPosition:selectedTextRange.end];
        replacePosition = selectedTextRange.start;
    }
    
    NSInteger beforeEditLength = toBeString.length - lengthNeedReplace;
    if (limit - beforeEditLength >= text.length) {
        return true;
    } else {
        //这里就需要替换了
        NSInteger maxLengthLeft = limit - beforeEditLength;
        
        NSString * replaceString = [text substringToIndex:maxLengthLeft];
        // 使用target的协议方法,插入文字, 使textViewDidChange能够被激活
        [target insertText:replaceString];
        
        // 设置光标到正确的位置
        dispatch_async(dispatch_get_main_queue(), ^{
            UITextPosition * p = [target positionFromPosition:replacePosition offset:maxLengthLeft];
            UITextRange * rr = [target textRangeFromPosition:p toPosition:p];
            [target setSelectedTextRange:rr];
        });
        
        return false;
    }
    return true;
};

弯弯

当我们拿到了具体信息是,其实可以直接把finalString拼好,像下面这样,但这样有一个问题,不但引用了具体类型,切结直接设置的text,无法激活textViewDidChange 回调方法, 因此选择了UITextInput的insertText:方法。

            NSInteger location = [target offsetFromPosition:target.beginningOfDocument toPosition:markedTextRange.start];
            toBeString = [toBeString stringByReplacingCharactersInRange:(NSMakeRange(location, lengthNeedReplace)) withString:@""];
            
            //这里就需要替换了
            NSInteger maxLengthLeft = limit - beforeEditLength;
            
            NSMutableString * afterString = [NSMutableString stringWithString: toBeString];
            NSString * replaceString = [text substringToIndex:maxLengthLeft];
            
            NSInteger replaceLocation = [target offsetFromPosition:target.beginningOfDocument toPosition:replacePosition];
            
            [afterString replaceCharactersInRange:NSMakeRange(replaceLocation, 0) withString:replaceString];
            
            [target performSelector:@selector(setText:) withObject:afterString];

UITextInput 协议

@protocol UITextInput <UIKeyInput>
@required

/* Methods for manipulating text. */
- (nullable NSString *)textInRange:(UITextRange *)range;
- (void)replaceRange:(UITextRange *)range withText:(NSString *)text;

/* Text may have a selection, either zero-length (a caret) or ranged.  Editing operations are
 * always performed on the text from this selection.  nil corresponds to no selection. */

@property (nullable, readwrite, copy) UITextRange *selectedTextRange;

/* If text can be selected, it can be marked. Marked text represents provisionally
 * inserted text that has yet to be confirmed by the user.  It requires unique visual
 * treatment in its display.  If there is any marked text, the selection, whether a
 * caret or an extended range, always resides within.
 *
 * Setting marked text either replaces the existing marked text or, if none is present,
 * inserts it from the current selection. */ 

@property (nullable, nonatomic, readonly) UITextRange *markedTextRange; // Nil if no marked text.
@property (nullable, nonatomic, copy) NSDictionary<NSAttributedStringKey, id> *markedTextStyle; // Describes how the marked text should be drawn.
- (void)setMarkedText:(nullable NSString *)markedText selectedRange:(NSRange)selectedRange; // selectedRange is a range within the markedText
- (void)unmarkText;
.
.
.
上一篇 下一篇

猜你喜欢

热点阅读