iOSで検索ワードの入力中に検索結果を表示するインクリメンタルサーチの導入方法

検索ワードを入力中にリアルタイムに検索結果を表示するインクリメンタルサーチ（=インクリメンタル検索）の導入方法をまとめます。

![インクリメンタルサーチsmall.png](https://qiita-image-store.s3.amazonaws.com/0/34488/9416268c-bb2e-f4eb-0341-4fcf255081b7.png)

#インクリメンタルサーチとは
検索したい単語を入力中に検索を行うことです。
[インクリメンタルサーチ - Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%AF%E3%83%AA%E3%83%A1%E3%83%B3%E3%82%BF%E3%83%AB%E3%82%B5%E3%83%BC%E3%83%81)

###メリット
- アプリが持つコンテンツを検索時にユーザに見せられる機会が増える（= より多くのコンテンツをユーザにアプローチできる）
- ユーザが長い文字を入力するときに途中入力の状態でも検索結果が表示できる
- ユーザが末尾の文字で入力ミスをしても途中までの入力が合っていれば検索結果を表示できる

###デメリット
- 検索の回数が増えるため負荷が増える（アプリ/サーバ）

デメリットである負荷に注意しつつ、メリットを最大限に活かして検索の利便性を向上させましょう。

#インクリメンタルサーチを行うタイミング
日本語入力には文字変換があるため、アルファベット入力とは異なるタイミングが存在します。
日本語入力の変換確定前と、確定後です。

1. 検索ワードを入力して確定前のタイミング
　（入力した文字に青い網掛けがある状態）
![スクリーンショット 2016-04-03 14.42.22.png](https://qiita-image-store.s3.amazonaws.com/0/34488/291816da-2e40-c5d6-e174-cdbdec907158.png)

2. 検索ワードを入力して確定ボタンを押下したタイミング
　（入力した文字に青い網掛けがない状態）
![日本語入力確定後.png](https://qiita-image-store.s3.amazonaws.com/0/34488/8caaa1b7-1da0-d331-c4ee-0b63d280046e.png)

本記事では1と２のタイミングでインクリメンタルサーチを行う方法を、それぞれ紹介します。

#インクリメンタルサーチの実装方法 （Objectvie-C / Swift）

検索窓の実装にUISearchBar、またはUISearchControllerのどちらを使用している場合でも可能です。
UISearchBarやUISearchController自体の実装方法については割愛します。
Appleのサンプルコードをご参照ください。
[Table Search with UISearchController](https://developer.apple.com/library/ios/samplecode/TableSearch_UISearchController/Introduction/Intro.html)



## 1. 検索ワードを入力したタイミングで検索
文字確定前のタイミングで検索を実行します。

![スクリーンショット 2016-04-03 14.42.22.png](https://qiita-image-store.s3.amazonaws.com/0/34488/95a8f653-2f06-9e14-a798-5e694b732c0f.png)


```objc:Objective-C
#pragma mark - UISearchBarDelegate

- (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    // 確定前の文字入力を検知します
    // return YESして文字入力が確定した後にsearchBarの文字を取得する処理を遅延実行します
    dispatch_time_t *when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(100 * NSEC_PER_MSEC));
    dispatch_after(when, dispatch_get_main_queue(), ^(void){
        [self searchBySearchBarText];
    });
    return YES;
}

- (void)searchBySearchBarText {
    // 検索バーから検索ワードを取り出す
    NSString *searchText = self.searchController.searchBar.text;
    if (searchText != nil && searchText.length == 0) {
        return;
    }
    [self search:searchText];
}

// 検索ワードを受け取ってサーバ通信 or ローカルのデータを絞り込む処理を行う
- (void)search:(NSString *)search {}
```

```swift:Swift
// MARK: UISearchBarDelegate

func searchBar(searchBar: UISearchBar, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    // 確定前の文字入力を検知します
    // return trueして文字入力が確定した後にsearchBarの文字を取得する処理を遅延実行します
    let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(100 * Double(NSEC_PER_MSEC)))
    dispatch_after(delayTime, dispatch_get_main_queue()) {
        self.searchBySearchBarText()
    }
    return true
}

func searchBySearchBarText() {
    // 検索バーから検索ワードを取り出す
    let searchText = searchController.searchBar.text!
    if searchText.isEmpty {
        // 検索ワードが空のとき
        return
    }
    search(searchText)
}

// 検索ワードを受け取ってサーバ通信 or ローカルのデータを絞り込む処理を行う
func search(text: String) {}
```

日本語入力確定前の文字入力を検知できるタイミングはshouldChangeTextInRangeのみです。
引数のsearchBarとreplacementTextは意図的に使用していません。
詳しくは後述のおまけを参照ください。



## 2. 検索ワードを確定したタイミングで検索
検索ワードを入力中に日本語入力を確定したタイミングで検索を行います。
![日本語入力確定後.png](https://qiita-image-store.s3.amazonaws.com/0/34488/b52b9925-1dbe-a54f-76ab-84864bfd1f17.png)

<!--
UISearchController(iOS8以上)を使用している場合

```objc:Objective-C
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
    [self search:searchController.searchBar.text];

// 検索ワードを受け取ってサーバ通信 or ローカルのデータを絞り込む処理を行う
- (void)search:(NSString *)search {}
```

```swift:Swift
// MARK: UISearchResultsUpdating

func updateSearchResultsForSearchController(searchController: UISearchController) {
    search(searchController.searchBar.text!)
}

// 検索ワードを受け取ってサーバ通信 or ローカルのデータを絞り込む処理を行う
func search(text: String) {}
```


UISearchBarを使用している場合

-->

```objc:Objective-C
#pragma mark - UISearchBarDelegate

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
    [self search:searchBar.text];
}

// 検索ワードを受け取ってサーバ通信 or ローカルのデータを絞り込む処理を行う
- (void)search:(NSString *)search {}
```


```swift:Swift
// MARK: UISearchBarDelegate

func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
    search(searchBar.text!)
}

// 検索ワードを受け取ってサーバ通信 or ローカルのデータを絞り込む処理を行う
func search(text: String) {}
```

##おまけ
検索ワードを入力したタイミングで検索　の中でshouldChangeTextInRangeの他のパラメータを使用していません。
「遅延実行をしなくてもsearchBarとrangeとtextを使用すれば入力された文字が分かるのだから、それらを使用して検索ワードの構築を行って検索すればよいのでは？」
と思われるかもしれませんが、現状不可能なようでした。
理由は下記です。

- 文字変換したときに変換前の文字と変換後の文字がくっついた状態になってしまう
- 確定前の状態で入力位置のカーソル移動をされた後に削除を行うと、rangeの値がカーソル位置で変わらず常に入力確定前の最後の位置を示す


参考ついでに、制限事項アリで遅延実行させない処理を記載しておきます。

```objc:Objective-C

- (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    NSString *searchText;
    if (text.length == 0) {
        // 文字が削除されたとき
        // 末尾の1文字を削除した文字を検索ワードとします
        // 制限事項：カーソルを移動して末尾でない文字を削除した場合でも末尾を削除した文字で検索を行います
        NSString *searchBarText = searchBar.text;
        NSUInteger *index = searchBarText.length - 1;
        searchText = [searchBarText substringToIndex:index];
    } else {
        // 文字入力されたとき
        // 制限事項：文字変換したときに変換前の文字と変換後の文字を結合した文字で検索してしまいます
        // 例："あい"を"愛"に変換したときsearchTextは"あい愛"になります
        // 変換後にfunc searchBar(..., textDidChange ...)が呼ばれるため、このタイミングでsearchを行ってしまうが、すぐに後追いでtextDidChangeのsearchを実行することで回避
        NSMutableString *searchBarText = [searchBar.text mutableCopy];
        [searchBarText insertString:text atIndex:range.location];
        searchText = searchBarText;
    }
    [self search:searchText];
    return YES;
}

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
    [self search:searchBar.text];
}

- (void)search:(NSString *)search {}
```

```swift:Swift
// MARK: UISearchBarDelegate

func searchBar(searchBar: UISearchBar, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    let searchText: String
    if text.isEmpty {
        // 文字が削除されたとき
        // 末尾の1文字を削除した文字を検索ワードとします
        // 制限事項：カーソルを移動して末尾でない文字を削除した場合でも末尾を削除した文字で検索を行います
        let searchBarText = searchBar.text!
        let index = searchBarText.endIndex.advancedBy(-1)
        searchText = searchBarText.substringToIndex(index)
    } else {
        // 文字入力されたとき
        // 制限事項：文字変換したときに変換前の文字と変換後の文字を結合した文字で検索してしまいます
        // 例："あい"を"愛"に変換したときsearchTextは"あい愛"になります
        // 変換後にfunc searchBar(..., textDidChange ...)が呼ばれるため、このタイミングでsearchを行ってしまうが、すぐに後追いでtextDidChangeのsearchを実行することで回避
        let searchBarText = NSMutableString(string: searchBar.text!)
        searchBarText.insertString(text, atIndex: range.location)
        searchText = searchBarText as String
    }
    search(searchText)
    return true
}

func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
    search(searchBar.text!)
}

// 検索ワードを受け取ってサーバ通信 or ローカルのデータを絞り込む処理を行う
func search(text: String) {}
```

#まとめ
検索の利便性を向上するインクリメンタルサーチをiOSで実装する方法を紹介しました。
インクリメンタルサーチを導入することでユーザにこれまで以上にコンテンツをアプローチしてみてはいかがでしょうか。

#参考
UISearchBarDelegateの公式リファレンス
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UISearchBarDelegate_Protocol/
検索の公式サンプル
https://developer.apple.com/library/ios/samplecode/TableSearch_UISearchController/Introduction/Intro.html