大家肯定都写过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;
.
.
.
网友评论