UISearchDisplayController と NSFetchedResultContoller を組み合わせる

2010年7月15日木曜日 | Published in | 4 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: UISearchDisplayController 調査

今回は UISearchDisplayController と NSFetchedResultContoller を組み合わせて Core Data 内のデータを検索できるようにしてみる。


情報


ネット上で情報を探したがあまりなかった。Stack Overflow の記事が参考になった。
How to use NSFetchedResultsController and UISearchDisplayController - Stack Overflow

基本的には UISearchDisplayDelegateメソッド内で画面で入力した文字を条件にして NSFetchedResultController を再検索すれば良い。ポイントは NSFetchedResultController のキャッシュ削除。
[NSFetchedResultsController deleteCacheWithName:@"UserSearch"];
あとで触れるがこれが無いと色々なところで問題が生じる。

以下、サンプルコードを作って確認してみた。


ソースコード


GitHubからどうぞ。
SearchSample at 2010-07-15 from xcatsan's iOS-Sample-Code - GitHub

雛形は Xcodeで新規プロジェクトを作るときに "Navigation-based Application" を選び、"Use Core Data for storage" にチェックを入れる。

これで簡単に CoreData が扱えるようになる。エンティティを作り直し、簡単なテストデータを詰めておく。
NSArray* titles = [NSArray arrayWithObjects:
         @"坊ちゃん", @"にごりえ・たけくらへ", @"唐草物語", @"風の歌を聴け", @"本格小説", nil];
  NSArray* authors = [NSArray arrayWithObjects:
         @"夏目漱石", @"樋口一葉", @"澁澤龍彦", @"村上春樹", @"水村美苗", nil];
  NSManagedObject* mo;
  
  for (int i=0; i < [titles count]; i++) {
   mo = [NSEntityDescription insertNewObjectForEntityForName:@"Book"
              inManagedObjectContext:self.managedObjectContext];
   [mo setValue:[titles objectAtIndex:i] forKey:@"Title"];
   [mo setValue:[authors objectAtIndex:i] forKey:@"Author"];
   [mo setValue:[NSDate dateWithTimeIntervalSinceNow:-86400*i] forKey:@"timeStamp"];
   
   error = nil;
   [self.managedObjectContext save:&error];
   if (error) {
    NSLog(@"%@", error);
   }
  }
Interface Builder で UITableViewを作り直した。テンプレートで生成されていた UITableViewを削除した後、新しく UITableViewを配置し File's Owner を Outlet(view)へ接続する。そしてそこへ "Search Bar And Search Display Controller" をドラッグ&ドロップする。

微調整の他、メインとなるのは UISearchDisplayDelegate の実装。先の Stack Overflow のコードを参考にこんな感じで実装してみた(それ自体、iPhone Dev Center のサンプル TableSearchが元になっている)。
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
 shouldReloadTableForSearchString:(NSString *)searchString
{
    [self filterContentForSearchText:searchString scope:
     [[self.searchDisplayController.searchBar scopeButtonTitles]
      objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];
 
    return YES;
}

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
 shouldReloadTableForSearchScope:(NSInteger)searchOption
{
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:
      [[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:searchOption]];

    return YES;
}

- (void)filterContentForSearchText:(NSString*)searchText
 scope:(NSString*)scope
{
    NSString *query = self.searchDisplayController.searchBar.text;
    if (query && query.length) {
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"Title contains[cd] %@", query];
        [self.fetchedResultsController.fetchRequest setPredicate:predicate];
  [NSFetchedResultsController deleteCacheWithName:@"UserSearch"];
    }
 
    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
        // Handle error
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        exit(-1);  // Fail
    }  
 
}
ポイントは2つのデリゲートメソッド内から共通に呼び出している filterContentForSearchText:scope: 。この中で NSFetchedResultController の再検索を行っている。


動作確認


さて動かしてみよう。起動するとテスト用データが表示される。
検索窓をタップする。
「坊ちゃん」の "坊" といれると即座に検索結果に反映される。
結果が0件の場合


キャッシュクリア


NSFetchedResultController は検索結果をキャッシュしている。 [参考] Cocoaの日々: -[NSFetchedResultsController performFetch:] でクラッシュ この為、検索条件を変えて再検索する場合には明示的にキャッシュをクリアする必要がある。例えば次のコードで NSFetchedResultController を作成している場合:
NSFetchedResultsController *aFetchedResultsController =
    [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
        managedObjectContext:self.managedObjectContext
          sectionNameKeyPath:nil cacheName:@"UserSearch"];
最後の @"UserSearch" がキャッシュ名を表している。これはアプリケーションのレベルでユニークとなる。これを再検索前にクリアしておく。
[NSFetchedResultsController deleteCacheWithName:@"UserSearch"];
なお、このキャッシュが残っていると次回起動時にも問題を引き起こす。例えば検索状態にしておき: そのまま一旦アプリを終了する。そして再起動すると:
SearchSample[21767:207] *** Terminating app due to uncaught exception
 'NSInternalInconsistencyException', reason: 'FATAL ERROR: The persistent
 cache of section information does not match the current configuration.
  You have illegally mutated the NSFetchedResultsController's fetch request,
 its predicate, or its sort descriptor without either disabling caching or
 using +deleteCacheWithName:'
エラーが出る。 これは以前のキャッシュが残っていてそれが不整合の原因となっている為。 これを防ぐには: 1. キャッシュを使わない (キャッシュ名指定を nil とする) 2. 起動時にキャッシュをクリアする のどちらかを取る。 2. の場合、UIViewController 内であれば viewDidLoad で行うのが良いと思われる。
[NSFetchedResultsController deleteCacheWithName:@"UserSearch"];

※アプリで使う場合にはもう少し複雑な処理が必要。例えばメモリ不足で viewの再作成が起こった場合の処理など。


- - - - -
今気がついたがテンプレートから作られるデフォルトのDBが SQLiteになっていた。MacOSXの時は XMLだった。

Responses

  1. 匿名
    2011年8月31日 21:54

    いつも参考にさせて頂いております。

    上記のコードを利用させて頂いているのですが、他のビューからpushViewで上記のコードのテーブルビューを表示しようとしているのですが、そうすると何故かサーチバーが表示されなくなってしまいます。

    どのように解決をすれば良いか心当たりがあれば教えて頂けないでしょうか?

  2. 匿名
    2011年8月31日 21:54

    いつも参考にさせて頂いております。

    上記のコードを利用させて頂いているのですが、他のビューからpushViewで上記のコードのテーブルビューを表示しようとしているのですが、そうすると何故かサーチバーが表示されなくなってしまいます。

    どのように解決をすれば良いか心当たりがあれば教えて頂けないでしょうか?

  3. xcatsan says:
    2011年9月1日 0:30

    こんばんは。

    もしかしたら RootViewController.xib が使われていない可能性があります。
    試しに1枚画面をはさみ、ボタンを押したらこのサーチバー付きのテーブルビューを表示するように改造してみました。
    pushViewController: に渡す view controller を次のようにするとサーチバーが表示されませんでした。
    RootViewController* viewController = [[RootViewController alloc] init];
    この時はデータも表示されていません。Xib が利用されず素の UITableViewController が作成され、表示された状態になっています。

    これを改善するには明示的に Xib ファイルを指定してあげます。
    RootViewController* viewController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil];

    これでサーチバーが出るようになり、データも表示されるようになりました。

    質問の問題が同じかわかりませんが参考まで。

    では。

  4. xcatsan says:
    2011年9月1日 0:30

    こんばんは。

    もしかしたら RootViewController.xib が使われていない可能性があります。
    試しに1枚画面をはさみ、ボタンを押したらこのサーチバー付きのテーブルビューを表示するように改造してみました。
    pushViewController: に渡す view controller を次のようにするとサーチバーが表示されませんでした。
    RootViewController* viewController = [[RootViewController alloc] init];
    この時はデータも表示されていません。Xib が利用されず素の UITableViewController が作成され、表示された状態になっています。

    これを改善するには明示的に Xib ファイルを指定してあげます。
    RootViewController* viewController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil];

    これでサーチバーが出るようになり、データも表示されるようになりました。

    質問の問題が同じかわかりませんが参考まで。

    では。

  5. 匿名
    2011年9月1日 2:34

    まさにどんぴしゃりなご回答をありがとうございます。

    1日詰まっていたので大変助かりました!

    これからもブログを参考にさせて頂きます!ありがとうございました!

  6. 匿名
    2011年9月1日 2:34

    まさにどんぴしゃりなご回答をありがとうございます。

    1日詰まっていたので大変助かりました!

    これからもブログを参考にさせて頂きます!ありがとうございました!

  7. xcatsan says:
    2011年9月5日 21:35

    こんばんは。
    解決できたようで何よりです。
    開発頑張ってください。
    ではでは。

  8. xcatsan says:
    2011年9月5日 21:35

    こんばんは。
    解決できたようで何よりです。
    開発頑張ってください。
    ではでは。

Leave a Response

人気の投稿(過去 30日間)