iOS-日常开发-文本框字符输入限制实现方法

1,034 阅读3分钟

iOS输入框字符输入限制

1. 有这样一个需求

a. 在公司项目开发中涉及到多国语言,但是在项目里产品经理要求限制一些国家语言里的特殊符号输入。

b. 只允许输入26个英文字母、英文标点符号。

2. 那我该怎么去实现呢?

1. 一般方案

通过UITextView、UITextField的delegate方法:

// UITextView的代理方法
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;

// TextField的代理方法
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string;

判断针对当前输入的字符返回YES或者NO来判断是否允许字符输入

2. 改进方案

上述方案需要在View、VC中分别实现代理方法,第一工作量会比较大、第二也不易于后期的维护所以有没有更好的解决方法呢?

a. 通过对-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string方法断点,查看函数调用栈发现,UIKit框架会在调用该代理方法前调用 -(BOOL)_delegateShouldChangeCharactersInTextStorageRange:(NSRange)range replacementString:text delegateCares:(BOOL)care ;

b. 所以可以对UITextField这个方法进行hook通过method-swizzling;

c. 同时通过类关联技术(Associated)添加两个成员变量:

// 枚举类型,设置当前UITextField支持的字符集合可通过|运算实现多个字符集合的支持
@property(nonatomic,assign)AvailableCharacterSet availableCharacterSet;
// UITextField 对象通过实现该方法,通过返回YES或者NO可以用以判断是否允许输入某个字符/字符串
@property**(**nonatomic,copy)BOOL (^isAvailableBlock)(NSString *text,BOOL result);

d. 在hook的方法里,通过availableCharacterSet、isAvailableBlock的设置判断是否允许某个字符的输入

3. 实现代码如下:

a. 支持的枚举值

#ifndef LimitInput_h
#define LimitInput_h

typedef NS_OPTIONS(NSUInteger, AvailableCharacterSet) {
    AvailableCharacterSetAll                = 0,   //所有字符
    AvailableCharacterSetLowerCaseLetters   = 1<<0,//小写字母 [a-z]
    AvailableCharacterSetUpperCaseLetter    = 1<<1,//大写字母 [A-Z]
    AvailableCharacterSetNumber             = 1<<2,//数字 [0-9]
    AvailableCharacterSetDecimalPad         = 1<<3,//带小数点的数字
    AvailableCharacterSetEnglishPunctuation = 1<<4,//英文标点符号
};

#endif /* LimitInput_h */

b. NSString+LimitInput类包含不同类型的字符集合,用正则表达式的方式判断输入的字符是否在设置的字符集合内

#import <Foundation/Foundation.h>
#import "LimitInput.h"

NS_ASSUME_NONNULL_BEGIN

@interface NSString (LimitInput)
-(BOOL)isValidWithAvailableCharacterSet:(AvailableCharacterSet)set;
@end

NS_ASSUME_NONNULL_END

#import "NSString+LimitInput.h"
static NSSet* lowerCaseLetters(){
    static NSSet *_lowerCaseLetters = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSArray *lowerCaseLetters = @[
            @"q", @"w", @"e", @"r", @"t", @"y", @"u", @"i", @"o", @"p",
            @"a", @"s", @"d", @"f", @"g", @"h", @"j", @"k", @"l",
            @"z", @"x", @"c", @"v", @"b", @"n", @"m"];
        _lowerCaseLetters = [NSSet setWithArray:lowerCaseLetters];
        
    });
    return _lowerCaseLetters;
}

static NSSet* upperCaseLetter(){
    static NSSet *_upperCaseLetter = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSArray *upperCaseLetter = @[
            @"Q", @"W", @"E", @"R", @"T", @"Y", @"U", @"I", @"O", @"P",
            @"A", @"S", @"D", @"F", @"G", @"H", @"J", @"K", @"L",
            @"Z", @"X", @"C", @"V", @"B", @"N", @"M"];
        _upperCaseLetter = [NSSet setWithArray:upperCaseLetter];
        
    });
    return _upperCaseLetter;
}

static NSSet* number(){
    static NSSet *_number = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSArray *number = @[
            @"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"0"
        ];
        _number = [NSSet setWithArray:number];
    });
    return _number;
}

static NSSet* decimalPad(){
    static NSSet *_decimalPad = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSMutableArray *mu_decimalPad = NSMutableArray.new;
        [mu_decimalPad addObject:number().allObjects];
        [mu_decimalPad addObject:@"."];
        NSArray *decimalPad = mu_decimalPad;
        _decimalPad = [NSSet setWithArray:decimalPad];
    });
    return _decimalPad;;
}

static NSSet* englishPunctuation(){
    static NSSet *_englishPunctuation = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSArray *englishPunctuation = @[
            @"`",
            @"~" ,
            @"!",
            @"@",
            @"#",
            @"$",
            @"%",
            @"^",
            @"&" ,
            @"*",
            @"(",
            @")",
            @"_" ,
            @"\\-",
            @"+",
            @"=",
            @"\\{",
            @"}",
            @"\\[",
            @"\\]",
            @"\\\\",
            @"|",
            @"<" ,
            @">" ,
            @",",
            @".",
            @"/",
            @"?",
            @";",
            @":",
            @"'",
            @"\"",
            @" ",
            @"\\n"
        ];
        _englishPunctuation = [NSSet setWithArray:englishPunctuation];
    });
    return _englishPunctuation;
}

static NSPredicate *getPredicateWithCharacterSet(AvailableCharacterSet set){

    static NSMutableDictionary<NSNumber*,NSPredicate*> *_inputLimitPredicates = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _inputLimitPredicates = NSMutableDictionary.new;
    });
    
    NSPredicate *predicate = _inputLimitPredicates[@(set)];
    if (predicate) {
        return predicate;
    }
    if (set == AvailableCharacterSetAll) {
        return nil;
    }
    NSMutableSet *muset = NSMutableSet.new;
    if (set&AvailableCharacterSetLowerCaseLetters){
        [muset addObjectsFromArray:lowerCaseLetters().allObjects];
    }
    if (set&AvailableCharacterSetUpperCaseLetter){
        [muset addObjectsFromArray:upperCaseLetter().allObjects];
    }
    if (set&AvailableCharacterSetNumber){
        [muset addObjectsFromArray:number().allObjects];
    }
    if (set&AvailableCharacterSetDecimalPad) {
        [muset addObjectsFromArray:decimalPad().allObjects];
    }
    if (set&AvailableCharacterSetEnglishPunctuation){
        [muset addObjectsFromArray:englishPunctuation().allObjects];
    }
    NSString *exp = [NSString stringWithFormat:@"[%@]*",[[muset allObjects] componentsJoinedByString:@""]];
    _inputLimitPredicates[@(set)] = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",exp];
    return _inputLimitPredicates[@(set)];
}

@implementation NSString (LimitInput)
-(BOOL)isValidWithAvailableCharacterSet:(AvailableCharacterSet)set{
    if (set == AvailableCharacterSetAll) {
        return YES;
    }
    return [getPredicateWithCharacterSet(set) evaluateWithObject:self];
}
@end

c. UITextField+LimitInput 实现对方法的hook,添加实现字符是否允许输入的逻辑

#import <UIKit/UIKit.h>
#import "LimitInput.h"
@interface UITextField (Category)
@property(nonatomic,assign)AvailableCharacterSet availableCharacterSet;

/**
 该block可拦截到当前输入的文本是否可用,并且可以更改不可用文本为可用文本
 */
@property(nonatomic,copy)BOOL (^isAvailableBlock)(NSString *text,BOOL result);
@end

#import "UITextField+LimitInput.h"
#import <objc/runtime.h>
#import "NSString+LimitInput.h"
static char UITextFieldAvailableCharacterSetKey;
static char UITextFieldIsAvailableBlockKey;


@implementation UITextField (Category)

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodExchange];
    });
}
-(AvailableCharacterSet)availableCharacterSet{
    return [objc_getAssociatedObject(self, &UITextFieldAvailableCharacterSetKey) integerValue];
}
-(void)setAvailableCharacterSet:(AvailableCharacterSet)availableCharacterSet{
    objc_setAssociatedObject(self, &UITextFieldAvailableCharacterSetKey, @(availableCharacterSet), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(BOOL (^)(NSString *, BOOL))isAvailableBlock{
    return objc_getAssociatedObject(self, &UITextFieldIsAvailableBlockKey);
}
-(void)setIsAvailableBlock:(BOOL (^)(NSString *, BOOL))isAvailableBlock{
    objc_setAssociatedObject(self, &UITextFieldIsAvailableBlockKey, isAvailableBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

+(void)methodExchange{
    SEL textFiledSEL = NSSelectorFromString(@"_delegateShouldChangeCharactersInTextStorageRange:replacementString:delegateCares:");
    if ([UITextField.new respondsToSelector:textFiledSEL] == NO) {
        return;
    }

    Method textFiledMethod = class_getInstanceMethod(UITextField.class, textFiledSEL);
    Method exTextFiledMethod = class_getInstanceMethod([self class], @selector(_ex_delegateShouldChangeCharactersInTextStorageRange:replacementString:delegateCares:));
    method_exchangeImplementations(textFiledMethod, exTextFiledMethod);
}
-(BOOL)_ex_delegateShouldChangeCharactersInTextStorageRange:(NSRange)range replacementString:text delegateCares:(BOOL *)cares{
    BOOL result = [self _ex_delegateShouldChangeCharactersInTextStorageRange:range replacementString:text delegateCares:cares];
    if (result == YES && self.availableCharacterSet != AvailableCharacterSetAll) {
        result = [text isValidWithAvailableCharacterSet:self.availableCharacterSet];
        if (self.isAvailableBlock) {
            return self.isAvailableBlock(text,result);
        }
        return result;
    }
    return result;
}
@end

UITextView也可以按照这个逻辑,去实现字符输入的限制。 :如有误,请大家多多指教十分感谢