NSSearchFieldに入力された文字でデータを検索する


# 概要
- `NSSearchField`を使用し、入力した単語でデータを検索して、絞り込み表示するものを作成しました。
    - 今回のものは最低限だけですが、公式のドキュメントをざっと見た所、検索履歴を残したり色々できそうでした。
    
![May-24-2019 16-12-22](https://i.imgur.com/YgBcwDR.gif)

# GitHub

https://github.com/pommdau/Qiita_NSSearchFieldSample

# コード全体

``` ObjectiveC
//  AppDelegate.m
//  NSSearchFieldSample

#import "AppDelegate.h"
#import "Function.h"

@interface AppDelegate ()
@property NSArray *allPokemonNames; // 全てのポケモン名
@property NSArray *pokemonNames;    // テーブルビューに表示するポケモン名
@property (weak) IBOutlet NSSearchField *searchField;   // 検索窓
@property (weak) IBOutlet NSTableView   *tableView;
@property (weak) IBOutlet NSWindow      *window;
@end

@implementation AppDelegate

- (void)awakeFromNib {
    [self loadPokemonNames];    // テーブルビューに表示するデータを作成する
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
}


- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

// テキストからポケモン名のデータを取得する
// またテーブルビューに表示するデータを作成する
- (void)loadPokemonNames {
    NSURL *pokemonList = [[[NSBundle mainBundle] resourceURL] URLByAppendingPathComponent:@"PokemonList"];
    NSMutableArray *pokemonNames = [NSMutableArray arrayWithArray:[Function loadFile:pokemonList]];
    _allPokemonNames = [NSArray arrayWithArray:pokemonNames];
    [self fontSearchFieldIsChanged:nil];
}

#pragma mark - NSSearch Field Method
// 検索窓のテキストに変更があった場合に呼ばれる
- (IBAction)fontSearchFieldIsChanged:(id)sender {
    _pokemonNames = [self createListWithSearchWord:_searchField.stringValue list:_allPokemonNames];
    [_tableView reloadData];
}

/**
 @brief 対象文字列の含まれる要素の配列を作成する
 @parama word 検索する文字列（※ひらがな・カタカナを区別しない）
 @param oldList 検索対象の配列
 @return 対象文字列が含まれる要素の配列
 */
- (NSArray *)createListWithSearchWord:(NSString *)word list:(NSArray<NSString *> *)oldList {
    NSString *searchWord = [word stringByApplyingTransform:NSStringTransformHiraganaToKatakana reverse:NO];  // ひらがなをカタカナに変換（検索時の統一のため）
    searchWord = [searchWord uppercaseString];  // 大文字へキャスト
    if (searchWord.length == 0) {
        return oldList; // 検索文字列がない場合は、元の配列をそのまま返す
    }
    NSMutableArray *newList = [NSMutableArray array];
    [oldList enumerateObjectsUsingBlock:^(NSString *element, NSUInteger idx, BOOL * _Nonnull stop) {
        NSString *fixedElement = [element stringByApplyingTransform:NSStringTransformHiraganaToKatakana reverse:NO]; // 日本語フォント名（ひらがなをカタカナに変換）
        fixedElement = [fixedElement uppercaseString];  // 大文字へキャスト
        
        NSRange searchResult = [fixedElement rangeOfString:searchWord];
        if (searchResult.location != NSNotFound) {
            [newList addObject:element];  // 対象文字列が含まれる場合
        }
    }];
    return newList;
}

#pragma mark - NSTableView data source
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView{
    return _pokemonNames.count;
}

- (NSView *)tableView:(NSTableView *)tableView
   viewForTableColumn:(NSTableColumn *)tableColumn
                  row:(NSInteger)row{
    NSString        *identifier = tableColumn.identifier;
    NSTableCellView *cellView   = [tableView makeViewWithIdentifier:identifier owner:self];
    cellView.textField.stringValue = _pokemonNames[row];
    return cellView;
}

@end
```

``` ObjectiveC
/**
 @brief     テキストファイルを読み込んで内容を配列で返す
 @param     sourceUrl テキストファイルのパスURL
 @return    行区切りの配列
 */
+ (NSArray *)loadFile:(NSURL *)sourceUrl {
    NSError  *loadError = nil;
    // ファイルの生データ
    NSString *source = [[NSString alloc] initWithContentsOfURL:sourceUrl
                                                      encoding:NSUTF8StringEncoding
                                                         error:&loadError];
    if (loadError) {
        NSLog(@"%@", [loadError localizedDescription]);
        return [NSArray array];
    }
    NSMutableArray *contents = [NSMutableArray array];     // return value
    [source enumerateLinesUsingBlock:^(NSString *line, BOOL * _Nonnull stop) {
        [contents addObject:line];
    }];
    return contents;
}
```

- 別クラス`Function.h/m`には、上記のメソッドを定義しているだけである。

- データはこちらからお借りしました。`PokemonList`をプロジェクト内に作成しています。
    - [ポケモンの種族値データシートの配布【第7世代】](http://rikapoke.hatenablog.jp/entry/pokemon_datasheet_gne7)

# コード説明
## TableView（ViewBase）の設定（StoryBoard）

![-w322](https://i.imgur.com/kunfFeo.jpg)

- `dataSource`と`delegate`をバインドしておく
## NSSearchFieldのテキストが変更されたとき
``` ObjectiveC
@property (weak) IBOutlet NSSearchField *searchField;   // 検索窓
```

- テキストを取得するため`@IBOutlet`を作成しバインディングをしておく

``` ObjectiveC
#pragma mark - NSSearch Field Method
// 検索窓のテキストに変更があった場合に呼ばれる
- (IBAction)fontSearchFieldIsChanged:(id)sender {
    _pokemonNames = [self createListWithSearchWord:_searchField.stringValue list:_allPokemonNames];
    [_tableView reloadData];
}
```

- `NSSearchField`をアクションにバインドした場合、中のテキストが変更されたときにアクションが呼ばれる・
- 下記のように少し一般化したメソッドを定義した。
- 結局やっていることとしては、リストと検索文字列を渡して新たに表示するデータをプロパティに格納し、テーブルビューを更新する、というだけ。

![-w711](https://i.imgur.com/fKBl55q.jpg)

## 特定の文字列が含まれる要素の配列を作成するメソッド

``` ObjectiveC
/**
 @brief 対象文字列の含まれる要素の配列を作成する
 @parama word 検索する文字列（※ひらがな・カタカナを区別しない）
 @param oldList 検索対象の配列
 @return 対象文字列が含まれる要素の配列
 */
- (NSArray *)createListWithSearchWord:(NSString *)word list:(NSArray<NSString *> *)oldList {
    NSString *searchWord = [word stringByApplyingTransform:NSStringTransformHiraganaToKatakana reverse:NO];  // ひらがなをカタカナに変換（検索時の統一のため）
    searchWord = [searchWord uppercaseString];  // 大文字へキャスト
    if (searchWord.length == 0) {
        return oldList; // 検索文字列がない場合は、元の配列をそのまま返す
    }
    NSMutableArray *newList = [NSMutableArray array];
    [oldList enumerateObjectsUsingBlock:^(NSString *element, NSUInteger idx, BOOL * _Nonnull stop) {
        NSString *fixedElement = [element stringByApplyingTransform:NSStringTransformHiraganaToKatakana reverse:NO]; // 日本語フォント名（ひらがなをカタカナに変換）
        fixedElement = [fixedElement uppercaseString];  // 大文字へキャスト
        
        NSRange searchResult = [fixedElement rangeOfString:searchWord];
        if (searchResult.location != NSNotFound) {
            [newList addObject:element];  // 対象文字列が含まれる場合
        }
    }];
    return newList;
}
```

- 検索する場合にひらがな・カタカナを区別しないようにするため、文字列比較時にはカタカナに統一している。
    - [[iOS 10] 全角文字列を半角文字列に変換する](https://dev.classmethod.jp/smartphone/ios-10-fullwidth-to-halfwidth/)
    - [（Apple）stringByApplyingTransform:reverse:
](https://developer.apple.com/documentation/foundation/nsstring/1407787-stringbyapplyingtransform?language=objc)
- 今回は関係ないが、同様に大文字・小文字を区別しないようにするため、文字列比較時には大文字に統一している。
    - [（Apple）uppercaseString](https://developer.apple.com/documentation/foundation/nsstring/1409855-uppercasestring?language=objc)
- 文字列検索には`rangeOfString`を用いる
    - [2012-11-19 ObjectiveCでNSString内の文字列検索をする方法のまとめAdd Star](http://d.hatena.ne.jp/eidolon/20121119)