NSTextFieldの文字数制限を行う


# 概要
- `NSTextField`の入力文字数に制限をかけたい。
- 今回は2種類を試す。1つは`NSTextField`、もう1つは`NSFormatter`のカスタムクラスを作ること。

# GitHub

- https://github.com/pommdau/Qiita_TextFieldWithCharacterNumberLimit
    - 今回作成したサンプルはこちらから。

# 方法1_NSTextFieldのカスタムクラスを作成する
## 実装

``` ObjectiveC:FirstWindowTextField.m
#import "FirstWindowTextField.h"

static const NSUInteger kCharLimit = 5; // 文字数制限

@interface FirstWindowTextField() <NSTextFieldDelegate>
@end

@implementation FirstWindowTextField

- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];
    
    // Drawing code here.
}

- (id) initWithCoder:(NSCoder *)decoder
{
    self = [super initWithCoder:decoder];
    if (self) {
        [self setDelegate: self];
    }
    return self;
}

// MARK:- NSTextFieldDelegate(実際はNSControl由来) Methods
- (void)controlTextDidChange:(NSNotification *)aNotification {
    NSString *newString = [self stringValue];
    if ([newString length] > kCharLimit) {
        [self setStringValue:[newString substringToIndex:kCharLimit]];
    }
}

@end
```

- `NSTextField`のサブクラスを作成し、xib側で`NSTextField`のクラスに指定。
- NSTextFieldDelegate(実際はNSControlのDelegate由来)のメソッドを用いて、テキストが入力される毎に呼ばれるメソッドを利用して文字数制限を行う。
- 入力後の文字数が制限をオーバーしていた場合、`substringToIndex:kCharLimit`で前から制限文字数を抜き出している。

``` ObjectiveC
controlTextDidChange:(NSNotification *)aNotification;
```

# 方法2_NSFormatterのカスタムクラスを作成する
## 参考
- [objective c - How to limit NSTextField text length and keep it always upper case? - Stack Overflow](https://stackoverflow.com/questions/827014/how-to-limit-nstextfield-text-length-and-keep-it-always-upper-case)  
    - アイデアはこちらから。
- [(旧) Cocoaの日々: NSFormatter](http://xcatsan.blogspot.com/2008/04/nsformatter.html)  
    - NSTextFieldへNSFormatterを適用する方法
- [Creating a Custom Formatter](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DataFormatting/Articles/CreatingACustomFormatter.html#//apple_ref/doc/uid/20000196-CJBFABHB)
    - Apple公式ドキュメント
- [isPartialStringValid:newEditingString:errorDescription:](https://developer.apple.com/documentation/foundation/nsformatter/1417993-ispartialstringvalid?language=objc)
    - Apple公式ドキュメント

## 実装（コード）

- `NSFormatter`のカスタムクラス、`CustomTextFieldFormatter`を作成する。

``` ObjectiveC::CustomTextFieldFormatter.m
//  CustomTextFieldFormatter.m
//  TextFieldWithCharacterNumberLimit

#import "CustomTextFieldFormatter.h"

@interface CustomTextFieldFormatter ()
@property NSUInteger charLimit; // 制限文字数
@end

@implementation CustomTextFieldFormatter
- (id)init {
    if(self = [super init]){
        [self setCharLimit:3];
    }
    return self;
}

/**
 @brief 部分文字列が有効かどうか - ユーザがキーを押す際に呼ばれる
 @param partialString 現在セル内にあるテキスト
 @param newString partialStringの変更が必要なときの置換文字列
 @return 部分文字列が有効かどうか
 */
- (BOOL)isPartialStringValid:(NSString *)partialString
            newEditingString:(NSString **)newString
            errorDescription:(NSString **)error {
    if ((int)[partialString length] > _charLimit) {
        *newString = [partialString substringToIndex:_charLimit];
        return NO;  // newStringでpartialStringを置換
    }
    return YES;
}

// MARK:- NSFormatter Methods
// NSFormatterのカスタムクラス作成に必須なメソッド

/**
 @brief セルのオブジェクトを文字列表現に変換（数値→文字列）
 */
- (NSString *)stringForObjectValue:(id)object {
    return (NSString *)object;
}

/**
 @brief 文字列をセルに関連付けられたオブジェクトに変換（文字列→数値）
 */
- (BOOL)getObjectValue:(id *)object
             forString:(NSString *)string
      errorDescription:(NSString **)error {
    *object = string;
    return YES;
}
@end
```

- 下記はユーザがキーを押す際に呼ばれるメソッドで、部分文字列が有効かどうかを返す。
- やっていることは方法1と同じように、制限文字数より多い場合、`newString`に制限文字数の文字列を代入している。

``` ObjectiveC
/**
 @brief 部分文字列が有効かどうか - ユーザがキーを押す際に呼ばれる
 @param partialString 現在セル内にあるテキスト
 @param newString partialStringの変更が必要なときの置換文字列
 @return 部分文字列が有効かどうか
 */
- (BOOL)isPartialStringValid:(NSString *)partialString
            newEditingString:(NSString **)newString
            errorDescription:(NSString **)error {
    if ((int)[partialString length] > _charLimit) {
        *newString = [partialString substringToIndex:_charLimit];
        return NO;  // newStringでpartialStringを置換
    }
    return YES;
}

```

- 下記は`NSFormatter`のカスタムクラスを作成する際にoverrideが必要なメソッド。

``` ObjectiveC
// MARK:- NSFormatter Methods
// NSFormatterのカスタムクラス作成に必須なメソッド
/**
 @brief セルのオブジェクトを文字列表現に変換（数値→文字列）
 */
- (NSString *)stringForObjectValue:(id)object {
    return (NSString *)object;
}

/**
 @brief 文字列をセルに関連付けられたオブジェクトに変換（文字列→数値）
 */
- (BOOL)getObjectValue:(id *)object
             forString:(NSString *)string
      errorDescription:(NSString **)error {
    *object = string;
    return YES;
}
```

## 実装(xib側)

- オブジェクトを挿入して、クラスを`CustomTextFieldFormatter`とする。

![-w1034](https://i.imgur.com/0LodVLp.jpg)


- フォーマッタにカスタムしたものを設定

![-w1043](https://i.imgur.com/4jomECJ.jpg)

## 課題

- 以下は通常動作はするのだが、文字制限を超える貼り付けの際に文字が全く入らないのが問題。
    - e.g. 3文字制限のとき、"あ" + "いうえお" → "あ"のまま。
    - "あいう"となってほしい。
- これを使ってうまくするやり方はあるのかな…。
- [isPartialStringValid:proposedSelectedRange:originalString:originalSelectedRange:errorDescription:](https://developer.apple.com/documentation/foundation/nsformatter/1415263-ispartialstringvalid)

``` ObjectiveC
/**
 @brief 無効な文字を入力しないようにする
 @param partialStringPtr 検証する新しい文字列
 @param proposedSelRangePtr 文字列が受け入れられるか置き換えられる場合に使用される選択範囲。
 @param origString 変更が提案される前の元の文字列。
 @param origSelRange 変更が行われる選択範囲。
 */
- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
       proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
              originalString:(NSString *)origString
       originalSelectedRange:(NSRange)origSelRange
            errorDescription:(NSString **)error {
    int size = (int)[*partialStringPtr length];
    if (size > _charLimit) {
        return NO;
    }
    return YES;
}
```