Study CoreData 21 ~結果を絞り込め!~ (コクチもあるよ)

こんちゃ!

先日、とぉ〜〜〜ってもハッピーなことがありました♬

…と言うのも、
僕がデザインしたアイコンがまたしてもAppStoreにひとつ並んだんですよぉ!

そのアプリは『mmCalender』というアプリです。
アプリの詳細については、下のリンクを見て頂くとして
今回のアイコンはかなり自由に作らせて頂きました。

それがこちら!

いやはや、関係者の方々には感謝感謝です!!
無料ですので是非一度、iTunesでもごらんくださいませですv(^〜^)v

mmCalendar(壁紙カレンダー): 壁紙にシンプルなカレンダーを追加させよう。無料。- iPhoneアプリとiPadアプリをおすすめするAppBank

▶ DesignWorksページへ
 
以上、告知でした。

注意:投稿者自身もCoraDataについて勉強中のため、このシリーズには誤りが含まれている可能性があります。もし、間違いに気付かれた方はコメント欄もしくはtwitterなどでご指摘いただけると幸いです<(_ _)>
また、開発環境はXcode3.2.3 iPhone SDK 4です。実機でのテストなどは自己責任でお願いいたします。

 
 

1. UISearchDisplayControllerについて

んだば、今日も始めていきましょうかいの〜。
今回は、前回作った検索バーを使った検索機能の追加をおっぱじめていきたいと思います。
 
 
まずは、UISearchDisplayControllerについてざっと説明すると、
テーブルビューの上にかぶさるような形で表示できる検索用View
って思っておけばいいんではないでしょうか?

ただ、実際の表示はViewControllerと共通のメソッド内で処理されます。
例えば、UISearchDisplayControllerでセルを表示する際には、
RootViewControllerの[tableView: cellForRowAtIndexPath:]が呼ばれます。
 
つまり、テーブルビュー表示関連のメソッド内では

 
if([self.searchDisplayController isActive]) {
  // 検索時のテーブルビューの表示
}
else {
  // 通常時のテーブル表示
}

って感じで分岐させて処理してあげればいいワケですね。
 
 
実はこの分岐処理は、前回の[viewWillAppear:]を変更したトコでやってたんですよ。

ってコトで、早速表示関連から実装していきましょうか?
まず、前提として[_searchResults]というプロパティが検索結果を保持しているコトは覚えておいてくださいね。
(検索結果の取得は後の方で出てきます。)
 
 

2. RootViewと仲良く同じメソッドで。

まずはこちらから。

前回やったように[viewWillAppear:]で
_isSearching = [self.searchDisplayController isActive];
としてあるので、分岐は基本的に[if (_isSearching)]でOKです。

ま、ココは見た通りなんで難しいところは無いですね^^;
 
 
あとはセルの表示ですが、検索結果はこんな感じで表示しようかと。

 
 
なので、コードはこんな感じで。

 
あら、こちらも特に説明するとこはなさそうです…
なんだか楽しちゃってるみたいですみませんねぇ^^;
 
 
で、もうひとつ。
検索ビューでセルをタップした時の画面遷移です。

ここはちょこっとだけ説明しときますね。
[_masterTask]への代入のところは問題ないと思うんですが、
self.selectedIndexPathへの代入は直にindexPathを渡さずに、
検索結果として表示されたオブジェクト(_masterTask)の“fetchedResultsControllerでのindexPath”を渡しています。
 
 
この[selectedIndexPath]は、以前にやったように[fetchedResults…]でのセルの移動を管理する“sectionChecker”で参照されるものだからです。
(…分かりづらいと思いますが、ココは深く考えなくてもダイジョブです^^;)

これで、表示関連はとりあえず終了!
 
 

3. デリゲートメソッドの使い方。

次にいよいよ機能の実装に入ります!
では、今回の検索で使うデリゲートメソッドたちをご紹介。

 
#pragma mark -
#pragma mark UISearchDisplayController Delegate Methods
// SearchDisprayControllerがフォーカスされた時
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller;

// テキストによる検索
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
       shouldReloadTableForSearchString:(NSString *)searchString;

// scopeButtonによる絞り込み
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
       shouldReloadTableForSearchScope:(NSInteger)searchOption;

// SearchDisprayControllerが非表示になる直前
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller;

// 検索終了後
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller;

以上の5つを使います。
ただし、scopeButtonについては基本的な検索機能を実装してからにしますね。

実装の仕方としては、まずひとつめのメソッドで検索開始直前の処理
ふたつめと3つめのメソッドでは、検索結果を取得
そして残りの2つのメソッドで後片付け。って感じになります。
 
 
では先に支度と後片付けを。

 
// SearchDisprayControllerがフォーカスされた時
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
	_isSearching = YES;
}

// SearchDisprayControllerが非表示になる直前
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller {
	
	_isSearching = NO;
	_searchBarIsVisible = YES;
}

// 検索終了後
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller {
	self.searchResults = nil;
	_selectedIndexPath = nil;
	[self.tableView reloadData];
}

このようにWillBegan…とWillEnd…では[_isSearching = YES/NO]ってしてます。

で、DidEnd…では保持した検索結果とindexPathを解放すればいいですね。
それと[tableView reloadData]してますが、
この後に検索ビューが非表示になって元のテーブルビューが表示されるので更新してあげる必要があるからです。
 
 
そこまでできたら、メインの検索結果の取得にいくのですが、
今回はもうひとつメソッドを作って
[shouldReloadTableForSearchString:]と[shouldReloadTableForSearchScope:]の両方の処理を
新しいメソッドで処理
させちゃいたいと思います。
 
 
そのメソッド名は
– (void)setSearchResultForSearchString:(NSString *)searchString scopeIndex:(NSInteger)scopeIndex
にしてみました。
 
 
じゃ、先にそのメソッドを呼び出させておきましょう。

 
- (void)setSearchResultForSearchString:(NSString *)searchString scopeIndex:(NSInteger)scopeIndex {
	// TODO: ここで検索結果を取得する。
}

// テキストによる検索
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchString:(NSString *)searchString {

	[self setSearchResultForSearchString:searchString scopeIndex:_searchBar.selectedScopeButtonIndex];
    return YES;
}

// scopeButtonによる絞り込み
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchScope:(NSInteger)searchOption {

	[self setSearchResultForSearchString:[_searchBar text] scopeIndex:searchOption];
    return YES;
}

 
 

4. NSPredicate再び (前編)

これで、やっとこさ検索のための準備が整いました!

トトノイマシタ! 
 
 
『検索と掛けまして、やせ我慢している人とときます。』

そのこころは…
 
 
どちらもすぐに答える(堪える)でしょう…<(_ _;)>

…。
 
  
 
はいっ。では気を取り直して^^;
検索結果の取得はこんなまずはこんな感じで試してみましょう。

 
- (void)setSearchResultForSearchString:(NSString *)searchString scopeIndex:(NSInteger)scopeIndex {
	
	NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title CONTAINS[cd] %@", searchString];
	
	self.searchResults = [[fetchedResultsController_ fetchedObjects] filteredArrayUsingPredicate:predicate];
}

ここでは[predicateWithFormat:]で@”title CONTAINS[cd] %@”としていますが、
“CONTAINS”というのは左辺の中に右辺が含まれていればYESを返す
という処理をしてくれます。

で、[cd]という部分は
[c]は、大文字小文字を区別しない
[d]は、発音区別記号(例えば “á”と”a”など)を区別しない
という条件を意味します。
 
 
この”CONTAINS”以外にも
@”month BETWEEN { 1 , 10 }”のようにして
“月が1から10の間ならYES”なんてことなどもできます。

この辺りのより詳しい内容についてはこちらのドキュメントが良いかと。(英語)
Predicate Format String Syntax
 
 
ま、今回はとりあえず
タイトルに検索文字列が含まれていればYES!ってコトにしときました。
そして、以前にも使った[filteredArrayUsingPredicate:]
検索結果だけをself.searchResultsに入れてやればOKですね。
 
 
そしたら、もうちょっとやっとくコトがあります。

例えば、検索結果Viewからデータが編集/削除されたりした場合
当然セルを移動させるメソッドに通知がいくわけですが、
このセルの移動は
fetchedResultsControllerでの表示順変更のためのindexPathが送られるので
検索View上でそのindexPathによる並び替えをしちゃいけないんです。

なのでこれらのメソッドは検索中ならスルーさせるようにする必要があります。

 
#pragma mark -
#pragma mark Fetched results controller delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {

	if (_isSearching)
		return;
	
    // 以下、非検索時の処理...
}

- (void)controller:(NSFetchedResultsController *)controller 
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {

	if (_isSearching)
		return;
	
    // 以下、非検索時の処理...
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {

	if (_isSearching)
		return;

    // 以下、非検索時の処理...
}

 
 
そして、最後に通知されるメソッドでは編集後のデータを元に再検索して、検索用テーブルビューをリロードします。

 
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {

	if (_isSearching) {
		
		[self searchDisplayController:_searchDisplay shouldReloadTableForSearchString:_searchBar.text];
		[self.searchDisplayController.searchResultsTableView reloadData];
		return;
	}

    // 以下、非検索時の処理...
}

そしたら、スワイプによる削除時の処理も対応させとかなきゃですね。

 
 
これで、まずはオッケーです!
ビルドして試してみましょ〜!!
 
 

5. 絞り込め!


 
どですか〜?うまいこと動いてますか?
 
 
ですが、コレだと
複数の単語を使った”AND検索”(Googleみたいな)はできませんし、
できればタグを使った絞り込み検索なんかも欲しいところですね〜。

まぁ、AND検索は後回しにするとして、まずは絞り込み検索を使ってみましょうか?
 
 
実は、ユーザーが指定したタグでの検索も考えたのですが、
CoreDataメインのこのシリーズには向かないのでは?と思い、もっとシンプルに使い方が分かるように
デフォルトのタグを利用した絞り込みを実装することにしました。
 
 
ってコトで、サンプルとして以下の3つのタグをデフォルトにしてみしょう。

・家でやるTodoにつけるタグ [@Home]
・仕事に関するTodoにつけるタグ [Works]
・お買い物リスト的に使うタグ [Shopping]

 
そして、その絞り込み用のボタンの表示に使うのがコレです。

これはスコープバーと言うものでUISeachBarの機能のひとつです。
 
 
では、早速コイツを追加してみます。

たったコレだけで表示は完了ですっ!
簡単ですね〜♬
 
 
そしたら、これらのボタンに対応するデフォルトのタグを用意しとかなくちゃですね!
 
 

6. デフォルトのデータを超簡単に作る方法。

CoreDataで扱っているデータの実態はSQliteだったりXMLだったりなので、アプリの初回起動時からデータを持たせるにはそれらを作らなきゃって思いがちですが、もっとずっと簡単な方法があります。

それは…
 
 
シミュレータでデータを作る。

って方法です。

どういう事かというと、
今作っているdotodoは当然CoreDataのアプリなので
保存時にはSQliteなりXMLなりのデータファイルにデータを保存している
ワケですね。なら、
そのデータを初めっからアプリに読み込ませてあげればいいだけじゃないの?
ってコト。
 
 
ま、やってみれば分かります^^;
 
 

7. データファイルをつくって取り出す。

まず、シミュレータから一旦dotodoを削除したら
念のため“すべてをクリーニング”して、まっさらな状態でビルドします。

次に、適当なTaskをひとつ作ります。

そしたら、Tagの編集ビューに移動して3つのタグを続けて追加します。

で、詳細ビューに戻ったら[Save]ボタンを押して保存します。

これでRootViewに戻ってますね。
最後に作ったTaskを削除してビルドを終了します。
 

ハイっ!!
コレでデータファイルは出来上がりましたよん!

 
 
…で、できたファイルがドコにあるかというと、
/Users/”ユーザ名”/Library/Application Support/iPhone Simulator/4.0/Applications/
(※”4.0″の場合です。)
 

この“Applications”の中にシミュレータ内の全アプリのデータが入っていますので、その中からDoTodoを探してください。
 
見つかったらその中の“Document”の中に…

このDoTodo.sqliteが実際のデータファイルです。
そいつをドラッグしてプロジェクトの”Resources”フォルダ辺りにコピーしちゃいましょう!

コピーが済んだら、一応ファイル名も“DefaultData.sqlite”に変更しておきます。
 
 
そしたら、初回起動時にこのファイルを読み込むようにしましょう。
ファイルを読む込む処理は
DoTodoAppDelegateの[persistentStoreCoordinator]でやってましたね。
こんな感じになってるはずです。

 
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    
    if (persistentStoreCoordinator_ != nil) {
        return persistentStoreCoordinator_;
    }
    
    NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"DoTodo.sqlite"]];
    
    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
	
	/*! データモデルエディタでのバージョン移行を反映させるためのオプションを設定 */
	NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
							 [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
							 [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
							 nil];
	
    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
 ........ 

 
この中のハイライトされている8行目のコードを以下のように変更します。

ここでやってる事はこんな感じです。

1. storePathにドキュメントディレクトリの”DoTodo.sqlite”までのパスを渡す。
2. fileManagerの[fileExistsAtPath:]を使って、(1)のファイルが存在するか確認する。
3. ファイルがなかったら、メインバンドルにある”DefaultData.sqlite”のパスを取得して…
4. [copyItemAtPath: toPath: error:]を使って、storePathに”DefaultData.sqlite”のデータをコピーする。

 
これで、起動時には先程つくった3つのタグがある事になります。
(正確に言うと”No Category”カテゴリーもあります。)
 
 
そしたら、もう一度アプリを削除&クリーニングして確認してみてくださいね。
 
 
確認できましたか?
うまくいっていれば、先程作ったTagが3つあるはずです。
 
 
かなり中途半端ですが、ちょっと長くなりすぎましたので続きはまた次回。
次回はTagによる絞り込みとAND検索に挑戦します!
(そしていよいよ最終回ですよ〜♬)
 
 
んだば、またぁ〜した〜ぁ\(^〜^)/

Study CoreData 21 ~結果を絞り込め!~ (コクチもあるよ)」への1件のフィードバック

  1. ピンバック: [夕刊] 24カラットの黄金iPhone 4カバーの動画。

コメントを残す