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

2014.04.02

この記事は公開されてから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のセルをカスタマイズする」 ことについて書きます。