限制UITextField字数的正确姿势

szlzhm 3年前
   <p>限制UITextField的字数应该是一个很常见的需求吧。前些时我们项目就有个,比方说用户名不能超过20个字符,实现也很简单,实现UITextFieldDelegate方法:</p>    <pre>  <code class="language-objectivec">- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string  {      return textField.text.length + string.length < 21;  }</code></pre>    <p>恩,这样一般也是没什么问题的,但是偏偏有不一般的情况。如果你的应用只用支持iPhone,那么你不用往下看了,上面的代码没什么问题,如果你要在iPad上面运行的话,那么这种情况下它会crash:</p>    <p>1、iOS 9以上的iPad,因为只有iOS 9以上的iPad上面的软键盘左上方才会有这三个按钮:undo,redo,paste;</p>    <p>2、输入字符达到最大20个不能再输入的时候,先点击paste按钮,然后点击undo按钮;</p>    <p><img src="https://simg.open-open.com/show/c49df93c9d321a172a56323d24a76c76.png"></p>    <p>QQ20160913-0.png</p>    <p>华丽丽地crash了,当测试告诉我这个bug的时候我还莫名其妙,手贱打开iPad微信去看看其他的应用有没有限制字数的UITextField,果然发现了一个地方:个人信息-名字</p>    <p><img src="https://simg.open-open.com/show/9f2e3d85d24cea263ba1b782b7f808fd.jpg"></p>    <p style="text-align:center">IMG_0041.jpg</p>    <p>然后照着输到最大字数,然后那三个按钮反复点,果然,微信crash了!看来不是我一个人有这个问题啊。</p>    <p>查看错误信息:</p>    <pre>  <code class="language-objectivec">**Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString substringWithRange:]: Range {20, 19} out of bounds; string length 20'**</code></pre>    <p>很清楚substringWithRange这个方法被调用的时候参数Range超界了,我没有调用这个方法,很显然是系统自己调用的。设置断点进去,仔细查看这个代理方法(- (BOOL)textField:(UITextField <em>)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString</em> )string)的逻辑,才发现以前理解太肤浅了。</p>    <p>1、输入简单字母的时候,比如当前内容是“123”, 输入4,那么代理方法触发时range参数是{3, 0},string是“4”,如果return yes,那么系统会调用substringWithRange这个方法改变{3, 0}位置的内容为“4”,由于{3, 0}本来就在末尾,所以这里就是简单的添加得到“1234”;</p>    <p>2、如果是联想到了英语单词等情况,比如下面的点击候选框中的单词"keeps",那么此时range是{0, 2},string为“keeps”,return yes的话就会用“keeps”替换掉{0, 2}位置的内容“ke”得到“keeps”;</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bae1fa421ad460229dc0907069650c73.png"></p>    <p style="text-align:center">QQ20160917-0.png</p>    <p>3、点击删除按钮的时候,string为"",此时range为要删除的字符内容的位置,比如下面的range为{15, 1},return yes的话同样是替换此处的“w”为“”,从而完成删除;</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bdfabd8bc16c84d55e9f7dfc71707366.png"></p>    <p style="text-align:center">QQ20160917-1.png</p>    <p>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参数是一样的;</p>    <p>那么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!</p>    <p>找到原因了,这么写就不会crash了:</p>    <pre>  <code class="language-objectivec">- (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;  }</code></pre>    <p>后面的代码是保证超界的时候返回NO,就不会去调用substringWithRange这个方法了。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/8a18baf0a252</p>    <p> </p>