[iOS] UITableViewのデータを検索する [4月からはじめるiPhoneアプリ #2]

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは!

前回の 「UITableViewをStoryboardで実装し、理解する」 から一歩進んだ 「UITableViewに表示したデータを検索する」 を今回は実装して行きます。

この記事は4月からiOSアプリエンジニアとして働く方、転向する方を対象としています。

「iOSアプリケーションを1度でも作ったことがある」、もしくは「入門書を1冊読んだことがある」方には特に参考になるような記事になると思います。

何故Storyboardを使うのか

早速 前回 と同様に新規プロジェクトを作ります。 今回もStoryboard上でUIを作っていきます。

何故UIをStoryboardで作るかというと、

「実行 → デバッグ → 画像・位置・色・重なり順の表示修正」

といったフローをデザインビューで作成出来るので、開発期間の短縮&コードの見通しがよくなるからです。

反面、ゲームアプリなどはiOS標準のUIをほとんど使いませんのでStoryboardを活かしにくいです。iOSアプリ全てに万能とは言えませんのでプロジェクトに合わせて採用するかを決めてください。

それでは本題に戻って、今回はUITableViewの上部に 「Search Bar and Search Display」 を配置しましょう。

テーブルビューを作る

SearchTableViewCell_01

2つのViewを配置して、ViewController.mへそれぞれ接続します。

SearchTableViewCell_02

データソースなどは前回と同じものを使います。

#import "ViewController.h"

@interface ViewController ()<UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UISearchDisplayDelegate>

/**
 *  Storyboardに配置したテーブルが紐づいてます
 */
@property (weak, nonatomic) IBOutlet UITableView *tableView;
/**
 *  Storyboardに配置したサーチバーが紐づいてます
 */
@property (weak, nonatomic) IBOutlet UISearchBar *searchBar;

/**
 *  テーブルに表示する情報が入ります
 */
@property (nonatomic, strong) NSArray *dataSourceiPhone;
@property (nonatomic, strong) NSArray *dataSourceAndroid;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // デリゲートメソッドをこのクラスで実装する
    self.tableView.delegate = self;
    self.tableView.dataSource = self;

    // テーブルに表示したいデータソースをセット
    self.dataSourceiPhone = @[@"iPhone 4", @"iPhone 4S", @"iPhone 5", @"iPhone 5c", @"iPhone 5s"];
    self.dataSourceAndroid = @[@"Nexus", @"Galaxy", @"Xperia"];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - TableViewDataSource delegate methods

/**
 *  テーブルに表示するデータ件数を返します(実装必須)
 *
 *  @param tableView テーブルビュー
 *  @param section   対象セクション番号
 *
 *  @return データ件数
 */
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSInteger dataCount;

    switch (section) {
        case 0:
            dataCount = self.dataSourceiPhone.count;
            break;
        case 1:
            dataCount = self.dataSourceAndroid.count;
            break;
        default:
            break;
    }
    return dataCount;
}

/**
 *  テーブルに表示するセクション(区切り)の件数を返します(任意実装)
 *
 *  @param  テーブルビュー
 *
 *  @return セクション件数
 */
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 2;
}

/**
 *  テーブルに表示するセルを返します(実装必須)
 *
 *  @param tableView テーブルビュー
 *  @param indexPath セクション番号・行番号の組み合わせ
 *
 *  @return セル
 */
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    // 再利用できるセルがあれば再利用する
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (!cell) {
        // 再利用できない場合は新規で作成
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                      reuseIdentifier:CellIdentifier];
    }

    switch (indexPath.section) {
        case 0:
            cell.textLabel.text = self.dataSourceiPhone[indexPath.row];
            break;
        case 1:
            cell.textLabel.text = self.dataSourceAndroid[indexPath.row];
            break;
        default:
            break;
    }

    return cell;
}

@end

ここまでは前回とほとんど同じです。

StoryboardでTableViewのStyleをGroupedにしておくとセクション区切りが出ますので、そちらもお忘れなく。

実行してみます。

SearchTableViewCell_03

iOSではよく見るというか、「電話」アプリをはじめとしたほとんどのアプリで見かけるUIのパーツができました。

UITableViewのデリゲートメソッドでセクション毎のヘッダーを付けたりもできますので、その辺はお好みでいじってみてください。

それでは本題の検索の処理について入ります。

検索処理を行う - UISearchDisplayController

まずは検索バーのプレースホルダーをStoryboardから設定します。

SearchTableViewCell_05

そして検索バーをタップした時に暗転した状態で出てくるTableViewControllerに検索結果を表示する準備をします。

さらに検索結果を格納するNSArrayをプロパティで宣言しましょう。

@property (nonatomic, strong) NSArray *dataSourceSearchResultsiPhone;
@property (nonatomic, strong) NSArray *dataSourceSearchResultsAndroid;

結果を表示するViewControllerはStoryboardで 「Search Bar and Search Display」を紐づけした時に自動で参照が追加されて Self(UIViewController)にsearchDisplayController として紐づけられています。

この検索用ViewControllerにアクセスしたければ

self.searchDisplayController

これでアクセスが可能です。

検索キーワードを入力した時だけ処理を分けたいのであれば

// 省略...

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSInteger dataCount;

    // ここのsearchDisplayControllerはStoryboardで紐付けされたsearchBarに自動で紐づけられています
    if (tableView == self.searchDisplayController.searchResultsTableView) {
        switch (section) {
            case 0:
                dataCount = self.dataSourceSearchResultsiPhone.count;
                break;
            case 1:
                dataCount = self.dataSourceSearchResultsAndroid.count;
                break;
            default:
                break;
        }
    } else {
        switch (section) {
            case 0:
                dataCount = self.dataSourceiPhone.count;
                break;
            case 1:
                dataCount = self.dataSourceAndroid.count;
                break;
            default:
                break;
        }
    }
    return dataCount;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 省略...

    // ここのsearchDisplayControllerはStoryboardで紐付けされたsearchBarに自動で紐づけられています
    if (tableView == self.searchDisplayController.searchResultsTableView) {
        // 検索中の暗転された状態のテーブルビューはこちらで処理
    } else {
        // 通常時のテーブルビューはこちらで処理
        switch (indexPath.section) {
            case 0: // iOS
                cell.textLabel.text = self.dataSourceiPhone[indexPath.row];
                break;
            case 1: // Android
                cell.textLabel.text = self.dataSourceAndroid[indexPath.row];
                break;
            default:
                break;
        }
    }

    return cell;
}

このようにして切り分けることができます。

あとは検索結果をdataSourceから絞り込めば完成です。

データを絞り込む

検索方法には色々なアルゴリズムがありますが、 「部分一致検索」 を今回は実装していきます。

「部分一致検索」は例えると「iPhone5s」を検索したい時に頭の中で

「探したいのは何とか"5"だったんだけど何だっけかな〜。う〜ん、思い出せないぞ・・・。」

といった時に検索バーに「5」と入力すると5に相当するキーワードをごっそり表示してくれる検索処理だと思って下さればよろしいです。

コレクションクラス(NSArray, NSDictionary, NSSet)はNSPredicateを使うと簡単に絞り込めます。

絞り込みたいタイミングで

- (void)filterContainsWithSearchText:(NSString *)searchText
{
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF contains %@", searchText];

    self.dataSourceSearchResultsiPhone = [self.dataSourceiPhone filteredArrayUsingPredicate:predicate];
    self.dataSourceSearchResultsAndroid = [self.dataSourceAndroid filteredArrayUsingPredicate:predicate];
}

を呼べばいいようにメソッドを定義しました。 predicateWithFormatのSELFはデータソース自体をさしています。SQL文のような感じですね。

このメソッドはサーチバーにキーワードを入力する度に呼び出したいです。1文字入力する毎に候補を絞り込みたいですよね。

検索バーの文字を編集する度に呼ばれるデリゲートメソッドがUISearchDisplayDelegateに用意されています。

- (BOOL)searchDisplayController:controller shouldReloadTableForSearchString:(NSString *)searchString
{
    // 検索バーに入力された文字列を引数に、絞り込みをかけます
    [self filterContainsWithSearchText:searchString];

    // YESを返すとテーブルビューがリロードされます。
    // リロードすることでdataSourceSearchResultsiPhoneとdataSourceSearchResultsAndroidからテーブルビューを表示します
    return YES;
}

テーブルビューに共通して言えることなのですが、データソースが変わった(検索で絞り込んだ、など)時は

[self.tableView reloadData]

などで、テーブルビューを都度読み込み直してあげないとデータが反映されません。

しかし、検索用のViewControllerということもあってか上記のメソッドにreturn YESだけで自動的にリロードされるので、とても便利ですね。

これで実行してみます。

SearchTableViewCell_06

※セクションのヘッダービューにアイコンを設定しました。

こんな具合に表示されていれば完成です。5を入力すると5を含むデータのみに絞りこまれていますね。

以上で 「UITableViewに表示したデータを検索する」 の実装が完了しました。

まとめ

今回はNSPredicateの使い方が特に重要でした。

コレクションクラス(NSArray, NSDictionary, NSSet)はiOS開発に関わらず、多くの言語で使用しますので、Javaなどを先に学んだことがある方は、「あ、iOSではこんな感じなのね。」と思われたのではないでしょうか。

searchDisplayControllerはどうやって自クラスに紐づけるのかと迷いましたが、 「Search Bar and Search Display」 コンポーネントを紐づけた時に、一緒に紐づけられていました。

「self.search」とタイプして候補が出てくるまで私は気づかなかったので、注意して下さい。

次回、 「UITableViewのセルをカスタマイズする」 ことについて書きます。

  • Takesi Nakamura

    検索を絞り込む部分で、具体的にどの部分にメソッドをいれたらいいのですか?初心者ですみません。どうぞお願いします。

    • Yasuhisa Arakawa

      Takesi Nakamura 様

      コメントありがとうございます。UITableViewは初めはみんな慣れないと思いますので、畏まる事はないです。
      以下に該当するソースをアップロードしていますのでご参考にして下さい。
      https://github.com/Yasuhisa/UITableViewCustomCellSample/blob/master/UITableViewCustomCellSample/ViewController.m

      以下の行数に注目して下さい。

      – 18行目 プロトコル準拠(UISearchDisplayDelegate)
      – 58行目 デリゲートの実装クラスをself(ViewControllerクラス)に指定
      – 182行目 デリゲートメソッドの実装

      リンク先の内容を具体的に申し上げますと、

      18行目UISearchDisplayDelegateを@interface部の内に宣言しているのと、58行目

      self.searchDisplayController.delegate = self;

      の記述によってこのクラスで、検索バーに文字が入力される度に以下のデリゲートメソッドが呼び出されます。

      – (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString

      デリゲートメソッドとは何かのイベントを他のクラスに実装を任せることです。今回の場合は、サーチバーに値が入力されたよというイベントをViewControllerクラスで実装することを18行目と58行目で宣言し、182行目から実装を書いています。

      こちらのメソッドで入力された値「searchString」を引数に[self filterContainsWithSearchText:searchString];を呼び出し、データソースに絞り込みをかけています。

      ですので、上記のメソッド内に絞り込み処理を書いておけばサーチバーに入力される度に絞り込み処理が行われることになります。

      • Takesi Nakamura

        分かりやすいご回答どうもありがとう御座いました。これで一歩プログラマーに近づいたと思います。

  • pings

    はじめまして。これを参考に実装させていただいています、ありがとうございます。

    dataSouceに日本語の文字列を入れると、検索結果が違うものが絞り込まれてしまうのですが、なぜでしょうか・・

    Githubのソースをダウンロードしたものは日本語でも正しく動いていたので、実装の際に何かが抜けているのかと思っています。よろしければご教示いただけますでしょうか。よろしくお願いします。

    • Yasuhisa Arakawa

      pings 様
      コメントありがとうございます。また参考にして頂き大変嬉しいです。

      違う物が絞り込まれてしまうと書かれておりますが、具体的にどのような文字列を入れるとどういった具合に絞り込まれるのでしょうか?

      絞り込んでいるメソッドの
      – (void)filterContainsWithSearchText:(NSString *)searchText

      こちらの箇所で、引数のserchTextをログ出力してみて、GitHubのものと同じかを確かめてみてください。

      • pings

        大変素早いご回答ありがとうございます。
        ログを出して試してみたところ、結果は同じでした。
        ちなみに、データソースを下記のようにし、

        self.dataSourceiPhone = @[@”iPhone 4″, @”iPhone 4S”, @”iPhone 5″, @”iPhone 5c”, @”iPhone 5s”, @”アイフォン”, @”林檎”];
        self.dataSourceAndroid = @[@”Nexus”, @”Galaxy”, @”Xperia”, @”アンドロイド”];

        「林檎」で検索すると、抽出結果は「iPhone 4」の1件でした。
        「ア」で検索すると、抽出結果は「iPhone 4」「Nexus」の2件でした。

        もう少し頑張ってみます。もし何かヒントになることがありましたら、ご教示いただけますと幸いです。
        よろしくお願いいたします。

        • Yasuhisa Arakawa

          pings様

          こちらでもシミュレーターと実機で確認したところ以下のようになりました。

          もしかしたら、GitHubに上がっているものと実装コードが違うのではないでしょうか?

          – (void)filterContainsWithSearchText:(NSString *)searchText

          {

          NSPredicate *predicate = [NSPredicate predicateWithFormat:@”SELF contains[c] %@”, searchText];

          self.dataSourceSearchResultsiPhone = [self.dataSourceiPhone filteredArrayUsingPredicate:predicate];

          self.dataSourceSearchResultsAndroid = [self.dataSourceAndroid filteredArrayUsingPredicate:predicate];

          }

          こちらの「self.dataSourceSearchResultsiPhone」に正しくセットされているか確認してください。(Resultsの方を新しいデータソースとしてセットします。)

          もしそこが間違っていなければ、

          – (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

          こちらの処理でResultsから結果を表示しているかを確認してみてください。

          このメソッド内で検索中のデータソースかを切り分けないと表示が変わりません。

          • pings

            大変遅い返信で申し訳ありません。(その後、事情により開発がストップしていました)

            内容を確認したところ、ご指摘とおり、Resultsから結果を表示していなかったことがわかり、無事解決しました。

            そもそもの「日本語の文字列のときに検索結果表示がおかしい」というのも当方の思い込みで、表示する際のdataSourceが違っていたのでした。

            思い込みで遠回りしていましたが、アドバイスのおかげでうまくいきました。
            本当にありがとうございました!

          • Yasuhisa Arakawa

            pings様

            解決したようで何よりです。こちらこそコメント頂きありがとうございました。
            またいつでもどうぞ。

  • grandsn0w

    こんにちは。ここに来るのは初めてです。

    TableViewの絞り込みまではソースコードの通り実現できたのですが、検索時にセルをタップしても画面が遷移しません。どんな記述をprepareForSegueやdidSelectRowAtIndexPathですればいいのでしょうか?よろしくお願いします。

    • Yasuhisa Arakawa

      grandsn0w 様

      こんにちは、初めまして。
      参考にして頂きありがとうございます。

      画面遷移が必要でしたらUINavigationControllerをStoryboardで使用する必要がございます。
      具体的にはこちらの記事を御覧ください。
      https://dev.classmethod.jp/smartphone/iphone/remind-storyboard/

      手順としては以下のとおりです。

      1. Storyboard上で遷移したい画面のUITableViewCell, もしくはUIViewControllerから遷移先のUIViewControllerへSegueをつなぐ(UITableViewCellから繋いだ場合、3. の手順は不要です。)
      2. 接続したSegueにStoryboardでIdentifierをセットする
      3. [self performSegueWithIdentifier:(セットしたIdentifier) sender:self];をdidSelectRowAtIndexPathに記述する
      4. 必要に応じて、prepareForSegueメソッドで選択したインデックスに該当するデータを次の画面にセットする([self.tableView indexPathForSelectedRow]で選択されたインデックスがprepareForSegueメソッド内でも取得できます。)

      まとめたものが以下の記事になっております。
      https://dev.classmethod.jp/smartphone/uitableview_navigationcontroller/

      参考になれば幸いです。