UITextFiledにInputFilter風のバリデーションメソッドを追加する

androidのEditTextクラスにはEditableインタフェースを経由して使うInputFilterインタフェースが用意されており、これを使って入力文字列のフィルタやバリデーションに使用することが出来た。

[android]EditableとInputFilter

iOSの場合、文字列自体を置き換えるようなフィルタを実装する方法は見つからなかったが※、同様にバリデーションを実行することは出来そうなので、表題の通りInputFilter"風"のメソッドを実装してみよう。

UITextFieldの拡張が1番それっぽいのだが、継承クラスを書くとそのベースクラスに依存してしまうのと、同様に拡張したクラスをInterfaceBuilderでも使わなければならない等面倒なことが多いので、ここはObjective-Cのカテゴリを使うことにする。

まずはヘッダからだ。

UITextField+InputFilter.h
#import <UIKit/UIKit.h>

@interface UITextField (InputFilter) <UITextFieldDelegate>
@property (strong, nonatomic) NSCharacterSet* filterCharSet;
@property (strong, nonatomic) NSRegularExpression* filterRegExp;
@property (        nonatomic) int maxLength;
/**
 * キャラクタセットによるバリデーションを実行します
 */
- (BOOL)validateForCharSet:(NSString*) targetString;
/**
 * 正規表現メタキャラクタによるバリデーションを実行します
 */
- (BOOL)validateForRegExp:(NSString*) targetString;
/**
 * 最大文字長によるバリデーションを実行します
 */
- (BOOL)validateForMaxLength:(NSString*) targetString;
@end

UITextField+InputFilter.hはをUITextFieldを拡張するカテゴリだ。
バリデーション方法は用途に応じて三種類用意すれば充分だろうか。

    • NSCharacterSetにマッチするか
    • NSRegularExpression(正規表現)にマッチするか
    • 文字列の最大超を超えていないか

これらのバリデーションをUITextFieldのデリゲートである-textField:shouldChangeCharactersInRange:replacementString:の実行時に行うようにする。

それぞれのバリデーション方法の基準になるプロパティを持つが、カテゴリはインスタンスプロパティを持てないので、おなじみのAssociatedObjectを使ってプロパティを実装することにする。

UITextField+InputFilter.m
#import "UITextField+InputFilter.h"
#import <objc/runtime.h>

#define TagKey_FilterCharSet "InputFilter_FilterCharSet"
#define TagKey_FilterRegExp "InputFilter_FilterRegExp"
#define TagKey_MaxLength "InputFilter_MaxLength"

@implementation UITextField (InputFilter)
@dynamic filterCharSet;
@dynamic filterRegExp;
@dynamic maxLength;

#pragma mark - UITextFieldDelegate デリゲート
#pragma mark -
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    return [self validateForCharSet:string]
        && [self validateForRegExp:string]
        && [self validateForMaxLength:string] ;
}

#pragma mark - バリデーションメソッド
#pragma mark -
- (BOOL)validateForCharSet:(NSString*) targetString
{
    if ( self.filterCharSet )
    {
        return ![targetString rangeOfCharacterFromSet:self.filterCharSet].location == NSNotFound;
    }
    else
    {
        return YES;
    }
}
- (BOOL)validateForRegExp:(NSString*) targetString
{
    if ( self.filterRegExp )
    {
        return [self.filterRegExp numberOfMatchesInString:targetString options:0 range:NSMakeRange(0, [targetString length])] > 0;
    }
    else
    {
        return YES;
    }
}
- (BOOL)validateForMaxLength:(NSString*) targetString
{
    if ( self.maxLength != 0 )
    {
        return self.text.length +  targetString.length <= self.maxLength;
    }
    else
    {
        return YES;
    }
}

#pragma mark - dynamic property accessor
#pragma mark -
- (NSCharacterSet*)filterCharSet
{
    NSCharacterSet* charSet =  (NSCharacterSet*)(objc_getAssociatedObject(self, TagKey_FilterCharSet));
    return charSet;
}
-(void)setFilterCharSet:(NSCharacterSet *)filterCharSet
{
    objc_setAssociatedObject(self, TagKey_FilterCharSet, filterCharSet, OBJC_ASSOCIATION_RETAIN_NONATOMIC );    
}
- (NSRegularExpression*)filterRegExp
{
    NSRegularExpression* regExp =  (NSRegularExpression*)(objc_getAssociatedObject(self, TagKey_FilterRegExp));
    return regExp;
}
-(void)setFilterRegExp:(NSRegularExpression *)filterRegExp
{
    objc_setAssociatedObject(self, TagKey_FilterRegExp, filterRegExp, OBJC_ASSOCIATION_RETAIN_NONATOMIC );
}
- (int)maxLength
{
    NSNumber* length =  (NSNumber*)(objc_getAssociatedObject(self, TagKey_MaxLength));
    return [length intValue];
}
-(void)setMaxLength:(int)maxLength
{
    objc_setAssociatedObject(self, TagKey_MaxLength, [[NSNumber alloc] initWithInt:maxLength], OBJC_ASSOCIATION_RETAIN_NONATOMIC );
}
@end

カテゴリで実装したので使い方は簡単だ。上記のヘッダをインクルードして、UITextFiledの生成後にバリデーションに使うプロパティとバリデーションを実施するデリゲートを設定するコードを書けば良い。

デリゲートは自身に設定することもできるが、その場合は三種のバリデーション結果の論理積が戻り値になる。

使用列 (とあるViewControllerにて)
    //バリデーションのための正規表現と最大文字長設定
    NSError* error = nil;
    textField1.filterRegExp = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z0-9]+$" options:0 error:&error]; //英数字のみ許可
    textField1.delegate = self;
    textField1.maxLength = 10; //最大10桁#pragma mark - UITextFieldDelegate デリゲート
#pragma mark -
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    return [textField validateForRegExp:string] && [textField validateForMaxLength:string];
}

この例ではバリデーションに成功しないと入力文字がキャンセルされるだけだが、Alertを表示する等の処理を書きたい場合はデリゲートメソッド中から呼び出せば良いだろう。