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

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

作者: 山已几孑 | 来源:发表于2021-08-05 16:43 被阅读0次

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

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

    写在前面

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

    • 初级:
      就告诉你在textViewshouldChangeTextInRange或者textViewDidChange相关的方法里面判断就完了,完了,完了

    • 中级:
      中级的知道告诉你,需要你判断一下你这是不是中文, 还知道判断一下markedTextRange有没有值(有值的话就是textVIew的text中存在正在联想中的输入)

    -(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;
    .
    .
    .
    

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/vobmvltx.html