限制UITextField字数的正确姿势

限制UITextField的字数应该是一个很常见的需求吧。前些时我们项目就有个,比方说用户名不能超过20个字符,实现也很简单,实现UITextFieldDelegate方法:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    return textField.text.length + string.length < 21;
}

恩,这样一般也是没什么问题的,但是偏偏有不一般的情况。如果你的应用只用支持iPhone,那么你不用往下看了,上面的代码没什么问题,如果你要在iPad上面运行的话,那么这种情况下它会crash:
1、iOS 9以上的iPad,因为只有iOS 9以上的iPad上面的软键盘左上方才会有这三个按钮:undo,redo,paste;
2、输入字符达到最大20个不能再输入的时候,先点击paste按钮,然后点击undo按钮;


QQ20160913-0.png

华丽丽地crash了,当测试告诉我这个bug的时候我还莫名其妙,手贱打开iPad微信去看看其他的应用有没有限制字数的UITextField,果然发现了一个地方:个人信息-名字


IMG_0041.jpg

然后照着输到最大字数,然后那三个按钮反复点,果然,微信crash了!看来不是我一个人有这个问题啊。

查看错误信息:

**Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString substringWithRange:]: Range {20, 19} out of bounds; string length 20'**

很清楚substringWithRange这个方法被调用的时候参数Range超界了,我没有调用这个方法,很显然是系统自己调用的。设置断点进去,仔细查看这个代理方法(- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string)的逻辑,才发现以前理解太肤浅了。
1、输入简单字母的时候,比如当前内容是“123”, 输入4,那么代理方法触发时range参数是{3, 0},string是“4”,如果return yes,那么系统会调用substringWithRange这个方法改变{3, 0}位置的内容为“4”,由于{3, 0}本来就在末尾,所以这里就是简单的添加得到“1234”;
2、如果是联想到了英语单词等情况,比如下面的点击候选框中的单词"keeps",那么此时range是{0, 2},string为“keeps”,return yes的话就会用“keeps”替换掉{0, 2}位置的内容“ke”得到“keeps”;


QQ20160917-0.png

3、点击删除按钮的时候,string为"",此时range为要删除的字符内容的位置,比如下面的range为{15, 1},return yes的话同样是替换此处的“w”为“”,从而完成删除;


QQ20160917-1.png

4、iPad上点击undo,redo,paste中的按钮都会触发该代理方法,比如点击paste时与前面1中情况比较类似,会在后面添加,点击undo的时候会撤销刚才的添加操作,此时与上面的操作3类似。如果刚才paste添加操作是range = {10, 2},string = "ww",那么就会在末尾添加“ww”,此时undo操作就会是range = {10, 2},string = “”,从而删掉刚才添加的“ww”。可以看出此时paste和undo操作range参数是一样的;
那么bug为什么会发生呢?如果没有在这个代理方法里面加上上面的代码,是不会有bug的,无论你增删改,上面的range都不会超过textField.text的范围,也就是系统调用substringWithRange这个方法的时候并不会有问题。但是如果加上了上面的代码,表明我们并不允许字符数超过20,当text内容恰好长度为20的时候,此时点击paste,会调用这个代理方法(比如string为"ww",range为{20, 2}),但是会返回NO。然后你再点击undo时,此时的range也为{20, 2},string为“”,此时会返回yes,然后系统会调用substringWithRange方法,range超界,crash!
找到原因了,这么写就不会crash了:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if (textField.text.length + string.length > 20) {
        return NO;
    }
    if (textField.text.length < range.location + range.length) {
        return NO;
    }
    
    return YES;
}

后面的代码是保证超界的时候返回NO,就不会去调用substringWithRange这个方法了。

推荐阅读更多精彩内容