万神劫

万物天地为剑,神鬼妖邪为剑
劫波万渡,宇宙苍穹尽为剑
是为万神劫!

2条评论 2015-10-22

为UITextField增加MaxLength特性

实现方案

在 HTML 的世界里,输入框天生就有 MaxLength 属性,可以限制用户输入的最大字符数量
可惜 iOS 上对应的 UITextField 并没有这样方便的属性,只有自己动手来实现
MaxLength 的实现并不难,如果去 StackOverflow 上搜一下,能看到大量的答案,比如这个回答,基本原理都是这样

- (BOOL)textField:(UITextField *) textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

    NSUInteger oldLength = [textField.text length];
    NSUInteger replacementLength = [string length];
    NSUInteger rangeLength = range.length;

    NSUInteger newLength = oldLength - rangeLength + replacementLength;

    BOOL returnKey = [string rangeOfString: @"\n"].location != NSNotFound;

    return newLength <= MAXLENGTH || returnKey;
}

即通过 textField:shouldChangeCharactersInRange:replacementString 方法,来决定用户是否还能继续输入
但是如果你直接抄了这段代码,一定会被细心的测试同学提 BUG,为什么呢?因为老外不用输入法啊!比如下图这种场景

在开启中文输入法时,如果当前只剩一个字符可以输入,那么想再输入一个汉字基本不太可能
因为打出的拼音也会出现在输入框里,而且会触发上述方法,那么咋办呢?直接看代码

// 添加监听
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(textFieldDidChanged:)
                                             name:UITextFieldTextDidChangeNotification
                                           object:self.textField];

// 监听处理
- (void)textFieldDidChanged:(NSNotification *)notification {
    NSString *text = self.textField.text;

    // 拼音输入时,拼音字母处于选中状态,此时不判断是否超长
    UITextRange *selectedRange = [self.textField markedTextRange];
    if (!selectedRange || !selectedRange.start) {
        if (text.length > MAXLENGTH) {
            self.textField.text = [text substringToIndex:MAXLENGTH];
        }
    }
}

这里主要使用了两个知识:

  • 输入法输入时,拼音字母或者笔画处于选中状态,可以使用 markedTextRange 获取到
  • 普通输入,以及将输入法的待选字填入输入框时,都会发出 UITextFieldTextDidChangeNotification, 可以监听这个通知,并事后对 UITextField 的内容做清理

另外除了使用监听 NSNotification 的方式,也可以使用 addTargetAction 的方式,代码如下:

[self.textField addTarget:self
                   action:@selector(textChange:)
         forControlEvents:UIControlEventEditingChanged];

这与上面的方式是等价的,但是使用 NSNotification 需要在对象销毁时 removeObserver,而这种方式不需要
下面讲封装时会提到这一点

如何封装

如果项目中很多地方都需要 MaxLength 特性,那么最好做一下封装,减少代码重复,那么怎么做呢?

子类化

首先想到的是创建 UITextField 的子类,在子类中增加一个 MaxLength 属性,并在用户设置属性时增加通知监听处理
这样所有的地方都使用这个子类就好啦,不过使用子类有这么几个缺点:

  • 一些额外的工作量,因为所有用到的地方都要改为这个子类
  • 由于 OC 不支持多重继承,那么如果将来还需要封装其他特性的话,就只能再创建这个子类的子类,灵活性会非常差

使用 Category

如果我们能直接为 UITextField 增加 maxLength 属性就好了
OC 中有 Category 语法,可以为已有的类增加新的方法,但并不支持增加新的属性(想念 Ruby 的 Mixin T_T)
幸运的是有折衷的办法能够实现,可以参考Associated Objects,这里就不赘述了,直接上代码

// UITextField+MaxLength.h

@interface UITextField (MaxLength)
@property (assign, nonatomic) NSUInteger maxLength;
@end


// UITextField+MaxLength.m

static char kMaxLength;
@implementation UITextField (MaxLength)
@dynamic maxLength;

- (void)setMaxLength:(NSUInteger)maxLength {
    objc_setAssociatedObject(self, &kMaxLength, @(maxLength), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if (maxLength) {
        // 代码
    }
}

- (NSUInteger)maxLength {
    NSNumber *number = objc_getAssociatedObject(self, &kMaxLength);
    return [number integerValue];
}

@end

我们在 setMaxLength 方法里面增加监听与处理的方法即可
如果使用的是 NSNotification,我们还需要在对象销毁时移除监听,在 Category 中增加这个方法

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

不过测试一下就知道,这么做是有问题的,因为我们覆盖了原生 UITextField 的 dealloc 方法,会导致应用崩溃
反而使用子类的话不会有问题,调用 [super dealloc] 即可

怎么办呢?两个方法,一是换用上面提到的 addTargetAction 方式,二是使用 Method Swizzling 这种高级用法,参考代码

//...
#import <objc/runtime.h>
#import <objc/message.h>

+ (void)load {
    Method origMethod = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
    Method newMethod = class_getInstanceMethod([self class], @selector(my_dealloc));
    method_exchangeImplementations(origMethod, newMethod);
}

- (void)my_dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self my_dealloc];
}

Swizzling 也不细说了,资料非常多,与 Ruby 中的 alias method chain 倒是挺像的

IB_DESIGNABLE

如果你像我一样,喜欢使用 Xib 的话,一定会希望能够在 Xib 的属性面板里增加这个属性
再次幸运的是,Apple 也提供了这样的方式,可以参考CreatingaLiveViewofaCustomObject
根据文章,只需要对 .h 文件略作修改即可

IB_DESIGNABLE

@interface UITextField (MaxLength)

@property (assign, nonatomic) IBInspectable NSUInteger maxLength;

@end

效果如图

最后,放一下代码,以供参考https://github.com/edokeh/UITextField-MaxLength

comments powered by Disqus