実数だけを入力させる UITextField

というものを作る必要があって、コードを書いていたのですが結構面倒でした。数字だけなら良かったのですが、以下の様な条件がありました。

  • マイナスの値も入力可
  • 値は整数ではなく実数

 これだけみると大したことは無さそうですが、意外と面倒なんです。条件を整理すると以下のようになります。

  • マイナス記号は先頭だけに入力
  • 小数点が一番後ろについていたらゼロを付加
  • 小数点が先頭だったらその前にゼロを付加(マイナス記号がついても同様)
  • 小数点は複数入力させてはならない
  • マイナス記号は複数入力させてはならない

 最後の2つは厄介。なぜならキーボード入力のみであれば1文字1文字をチェックすればいいのですが、copy & paste という入力があるからです。

 マイナス記号を入力したかったのでキーボードのタイプは UIKeyboardTypeNumbersAndPunctuation の一択。

 UITextField の入力内容を修正するタイミングとしては2つ。ユーザーが入力を行ったとき、それから入力が終了した時。それぞれ以下のメソッド(セレクタ)を使います。delegate とかそういう話は他のサイトやなんかで拾ってきてください。

// 入力時
/**
 * @param textField 変更前の状態の textField
 * @param range     新しい文字列(string)が挿入される位置
 * @param string    range の位置に挿入される文字列
 *
 * @return    新しい文字列を挿入して良いか
 */
- (BOOL)textField:(UITextField *)textField 
    shouldChangeCharactersInRange:(NSRange)range 
                replacementString:(NSString *)string;

// 入力終了時
- (void)textFieldDidEndEditing:(UITextField *)textField;

// ついでに入力開始時
- (void)textFieldDidBeginEditing:(UITextField *)textField

  始めは正規表現が面倒で、if 文を書きなぐっていたのですが、やっぱり条件が複雑すぎて混沌としてしまいました。というわけで正規表現利用に切り替え。

 参考サイト:
http://stackoverflow.com/questions/1434568/how-to-verify-input-in-uitextfield-i-e-numeric-input
http://regexlib.com/DisplayPatterns.aspx?cattabindex=2&categoryId=3(正規表現全般)

 方針としては以下のようになりました。

  • 入力時に数字と記号類の並びは整える
  • 入力終了時に整数部先頭のゼロ連続を除去する(ex: 0012 → 12)
  • 入力終了時に入力値がゼロ度の場合は0.0に
  • 入力終了時に入力値が + – . 入力なしの場合は0.0に
  • -. は -0. に修正、+. は +0. に修正(ex: -.23 → -0.23)

 実際のコードを見てみましょう。まずは入力時。

- (BOOL)textField:(UITextField *)textField 
    shouldChangeCharactersInRange:(NSRange)range 
                replacementString:(NSString *)string
{
    // 入力後の文字列
    NSString *newString =
        [textField.text
            stringByReplacingCharactersInRange:range
                                    withString:string];

    NSString *expression = @"^[-+]?([0-9]*)?(\\.)?([0-9]*)?$";
    NSError *error = nil;

    NSRegularExpression *regex =
        [NSRegularExpression
            regularExpressionWithPattern:expression
            options:NSRegularExpressionCaseInsensitive 
            error:&error];

    NSUInteger numberOfMatches =
        [regex numberOfMatchesInString:newString
            options:0
            range:NSMakeRange(0, [newString length])];

    if (numberOfMatches == 0) return NO;

    return YES;
}

  正規表現の ^[-+]?([0-9]*)?(\\.)?([0-9]*)?$ について説明してみると、

  •  ^ : 先頭に
  • [-+]? : マイナスかプラスの符号が0個もしくは1個あり、
  • ([0-9]*)? : 続いて「数字が連続するカタマリ」が0個もしくは1個あり、
  • (\\.)? : 続いて小数点が0個もしくは1個あり、
  • ([0-9]*)? : 続いて「数字が連続するカタマリ」が0個もしくは1個あり、
  • $ : 最後に達する

となります。入力後の文字列がこの条件にあう文章なのであれば、入力してもよいよ(=return YES) となります。

 これでおおよそは整いました。少し困るのは00123のように整数部の先頭にゼロが付く場合、それから56.00のように小数部の末尾にゼロが付く場合。後者に関しては今回はよしとしました。正確性の明示という意味では悪くないという判断。めんどくさかったわけではありません。

 整数部の先頭のゼロは入力終了時に修正することにして、コードは以下のようになりました。ちなみに角度入力だったので入力開始時に角度記号の除去、入力終了時に角度記号の付加を行なっています。

// 入力開始時のメソッド
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    // 角度記号の除去
    textField.text =
        [textField.text substringToIndex:textField.text.length - 1];
}

// 入力終了時メソッド
- (void)textFieldDidEndEditing:(UITextField *)textField
{   
    // 先頭のゼロの連続を除去
    // (整数部がゼロで少数がある場合は下流でゼロ付加)
    NSString *expression = @"^[-+]?(0+)";
    NSError *error = nil;

    NSRegularExpression *regex =
        [NSRegularExpression
            regularExpressionWithPattern:expression
            options:NSRegularExpressionCaseInsensitive
            error:&error];

    if (error) NSLog(@"error:%@", error);
    NSArray *list =
            [regex matchesInString:textField.text
                   options:0
                   range:NSMakeRange(0, textField.text.length)];

    if ([list count] > 0)
    {
        NSTextCheckingResult *res = [list objectAtIndex:0];

        // 見つかった範囲を出力してみる
        for (int i=0; i<res.numberOfRanges; i++)
        {
            NSRange range = [res rangeAtIndex:i];
            NSLog(@"range[%d] : %d - %d",
                  i , range.location, range.length);
        }

        // 正規表現で( )の箇所の範囲の1つめ
        NSRange range = [res rangeAtIndex:1];

        textField.text = [textField.text
            stringByReplacingCharactersInRange:range
                                    withString:@""];
    }

    // 小数点後の末尾のゼロの連続の修正 ---> 行わない

    // 最後のまとめ処理
    if ([textField.text isEqualToString:@""]
        || [textField.text isEqualToString:@"-"]
        || [textField.text isEqualToString:@"+"]
        || [textField.text isEqualToString:@"."]
        || [textField.text floatValue] == 0.0f)
    {
        textField.text = @"0.0";
    }
    else
    {
        // 符号と小数点がならんでいるならゼロを挿入
        textField.text = [textField.text
                stringByReplacingOccurrencesOfString:@"-."
                                          withString:@"-0."];
        textField.text = [textField.text
                stringByReplacingOccurrencesOfString:@"+."
                                          withString:@"+0."];

        NSRange dotSearch = [textField.text rangeOfString:@"."];

        // 一文字目が小数点なら先頭にゼロを付加
        if (dotSearch.location == 0) {
            textField.text = [[NSString stringWithString:@"0"]
                    stringByAppendingString:textField.text];
        }

        // 最後が小数点ならば末尾にゼロを1つ追加
        if (dotSearch.location == textField.text.length - 1) {
            textField.text =
                    [textField.text stringByAppendingString:@"0"];
        }
    }

    // 角度記号の追加
    textField.text = [textField.text stringByAppendingString:@"°"];
}

  ※エラー処理はちゃんとはしてません。正規表現がただしければ問題ないかな?

 インデント調整してみましたが、Objective-C はやっぱりメソッド名が長すぎる!長い名前はなれると読みやすいは読みやすいですが、こういうところでは悩ましい。もちろんいつもはこんな変なインデントしてませんよ。以上余談。

 これで 00123 になっていても終了時に 123° に修正されます。符号と小数点が並んでいてもOK。これでおおよそ大丈夫になったと思います。想定外の入力はもうないはず。。。

  もっと考えるならば、角度なので -180°〜180° の間にまるめてもよかったのですが、 360° とか 540° などの入力を制限したくなかったので、数値の範囲制限は無しとしました。

 以上。実数だけを入力させる方法の紹介でした。入力終了時の処理はもう少しコンパクトにまとまりそうですが、そこは宿題ということで。

 (追記)正規表現マッチ部の取得方法が今ひとつだった場所を修正しました。参考サイト:http://d.hatena.ne.jp/hypercrab/20111224/1324658693

 

 本記事内容についての正確性については保証しかねますので実際のところは各自リファレンスなどでご確認ください。


カテゴリー: tech タグ: , , パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA