Help us understand the problem. What is going on with this article?

Appleのサンプルで学ぶUISearchDisplayControllerの使い方

More than 5 years have passed since last update.

Appleの公開している2つのサンプルでUISearchDisplayControllerの使い方をメモしておく。

UISearchDisplayControllerとは

検索画面で検索したらリアルタイムに候補がUITableViewに表示されるやつ。

サンプルについて

Appleは2つのサンプルを公開している。CoreDataなどを使わないシンプルなものなのでこれを読むのが一番良い。

Simple UISearchBar with State Restoration

IMG_1632.PNG

サンプルプロジェクトの画面はシンプルでリストと詳細しかない。このリスト部分で全てのデータを表示し、検索文字列の入力に応じて表示されるデータが絞られる形式となっている。

スクリーンショット 2014-02-24 20.10.55.png

動作的にはApple製品のラインナップ名がデスクトップやラップトップなど区分ごとに検索できるようになっている。

データの用意

検索するためのデータはappDelegateで用意している。

-(BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSArray *productArray = @[[APLProduct productWithType:ProductTypeDevice name:@"iPhone"],
                         [APLProduct productWithType:ProductTypeDevice name:@"iPod"],
                         [APLProduct productWithType:ProductTypeDevice name:@"iPod touch"],
                         [APLProduct productWithType:ProductTypeDevice name:@"iPad"],
                         [APLProduct productWithType:ProductTypeDevice name:@"iPad mini"],
                         [APLProduct productWithType:ProductTypeDesktop name:@"iMac"],
                         [APLProduct productWithType:ProductTypeDesktop name:@"Mac Pro"],
                         [APLProduct productWithType:ProductTypePortable name:@"MacBook Air"],
                         [APLProduct productWithType:ProductTypePortable name:@"MacBook Pro"]];

    UINavigationController *navigationController = (UINavigationController *)[self.window rootViewController];
    APLViewController *viewController = [navigationController.viewControllers objectAtIndex:0];
    viewController.products = productArray;

    return YES;
}

APLProductというクラスを用意し、そのインスタンスをNSArrayでまとめ、リスト用のViewController(APLViewController)で保持する。

検索対象となる文字列はNSArrayであればよいということが分かる。

APLProductクラスにはproductWithType:name:メソッドがあり、検索する区分をproductWithTypeで指定し、検索する名前をnameで指定するようになっている。

APLViewController

インターフェース

APLViewControllerは検索対象となる文字列を含むデータをproducsプロパティで保持している(正確にはメンバ変数_producsで保持していると言ったほうがよいかもしれないけど)。

@interface APLViewController : UITableViewController <UISearchDisplayDelegate, UISearchBarDelegate>

@property (nonatomic) NSArray *products; // The master content.

@end

UISearchDisplayDelegateは検索文字の入力時に呼ばれるメソッドのためだが、UISearchBarDelegateはここで記述されているものの実装はされていなかった...。

実装

privateなメンバ変数で検索結果を保持

無名カテゴリは以下のとおり、NSMutableArrayなsearchResultsプロパティを結果として保持するのだと思う。

@interface APLViewController ()

/*
 The searchResults array contains the content filtered as a result of a search.
 */
@property (nonatomic) NSMutableArray *searchResults;

@end

UITableViewのDataSourceで表示を制御

APLViewControllerのUITableView DataSourceにてテーブル数を返すメソッドで2つのテーブルを区別してそれぞれの数を返しているのが分かる。
片方は検索結果を返すために無名カテゴリで宣言したsearchResultsの数と、最初に初期化した検索対象の文字列を含むNSArrayの数だ。

#pragma mark - UITableView data source and delegate methods

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    /*
     If the requesting table view is the search display controller's table view, return the count of
     the filtered list, otherwise return the count of the main list.
     */
    if (tableView == self.searchDisplayController.searchResultsTableView)
    {
        return [self.searchResults count];
    }
    else
    {
        return [self.products count];
    }
}

cellへの表示も上記のようにtableViewを分けて処理を書いているだけなので省略。

UISearchDisplayControllerのdelegate(UISearchDisplayDelegate)を実装する

検索を行うためには動作するUISearchDisplayControllerのdelegateを実装している。下記コードに説明のコメントを追記した。

#pragma mark - UISearchDisplayController Delegate Methods

//検索欄に入力された文字列がsearchStringとして渡される
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchString:(NSString *)searchString
{
    //検索の区分を保持する。
    NSString *scope;

    //選択されている区分のindexを取得
    NSInteger selectedScopeButtonIndex = [self.searchDisplayController.searchBar selectedScopeButtonIndex];
    if (selectedScopeButtonIndex > 0)
    {
        //検索する区分を識別
        scope = [[APLProduct deviceTypeNames] objectAtIndex:(selectedScopeButtonIndex - 1)];
    }

    //区分と入力された文字列から検索を行う
    [self updateFilteredContentForProductName:searchString type:scope];

    // Return YES to cause the search result table view to be reloaded.
    return YES;
}

検索部分の実装

検索は単純に文字列のあるなしを見ているだけだった。検索対象に文字列があればself.searchResultsに追加している。

#pragma mark - Content Filtering

- (void)updateFilteredContentForProductName:(NSString *)productName type:(NSString *)typeName
{
    //検索対象の文字列がない場合
    if ((productName == nil) || [productName length] == 0)
    {
        // If there is no search string and the scope is "All".
        if (typeName == nil)
        {
            //検索区分もなしなのでそのまま表示
            self.searchResults = [self.products mutableCopy];
        }
        else
        {
            // If there is no search string and the scope is chosen.
            NSMutableArray *searchResults = [[NSMutableArray alloc] init];
            for (APLProduct *product in self.products)
            {
                if ([product.type isEqualToString:typeName])
                {
                    [searchResults addObject:product];
                }
            }
            self.searchResults = searchResults;
        }
        return;
    }


    [self.searchResults removeAllObjects]; // First clear the filtered array.
    /*
     Search the main list for products whose type matches the scope (if selected) and whose name matches searchText; add items that match to the filtered array.
     */
    //検索対象のNSArrayから愚直に列挙していく
    for (APLProduct *product in self.products)
    {
        //まず区分があっているかどうかを識別
        if ((typeName == nil) || [product.type isEqualToString:typeName])
        {
            //NSRangeで単純に文字列のマッチする範囲があるかどうかを行っている

            //NSCaseInsensitiveSearchオプションで大文字小文字を無関係に
            //NSDiacriticInsensitiveSearchオプションでラテン文字での区別されるべきものを区別するために指定
            NSUInteger searchOptions = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch;

            NSRange productNameRange = NSMakeRange(0, product.name.length);
            NSRange foundRange = [product.name rangeOfString:productName options:searchOptions range:productNameRange];
            if (foundRange.length > 0)
            {
                //文字列がマッチしていれば検索結果のためのNSMutableArrayに追加
                [self.searchResults addObject:product];
            }
        }
    }
}

Advanced UISearchBar

前出のものとの機能的な違いは、入力された文字列をスペース区切りでAND検索。数字なら値段と年をOR検索できることだが、画面的には違いがほとんどない。

IMG_1633.PNG

画面構成は前出のものと変わりがなかったので説明を割愛。

データの用意

こちらのAPLProductクラスはyearとpriceのプロパティが増えている。

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSArray *productArray = @[[APLProduct productWithType:ProductTypeDevice
                                                    name:@"iPhone"
                                                    year:[NSNumber numberWithInteger:2007]
                                                   price:[NSNumber numberWithDouble:599.00]],
                             [APLProduct productWithType:ProductTypeDevice
                                                    name:@"iPod"
                                                    year:[NSNumber numberWithInteger:2001]
                                                   price:[NSNumber numberWithDouble:399.00]],
                             [APLProduct productWithType:ProductTypeDevice
                                                    name:@"iPod touch"
                                                    year:[NSNumber numberWithInteger:2007]
                                                   price:[NSNumber numberWithDouble:210.00]],
                             [APLProduct productWithType:ProductTypeDevice
                                                    name:@"iPad"
                                                    year:[NSNumber numberWithInteger:2010]
                                                   price:[NSNumber numberWithDouble:499.00]],
                             [APLProduct productWithType:ProductTypeDevice
                                                    name:@"iPad mini"
                                                    year:[NSNumber numberWithInteger:2012]
                                                   price:[NSNumber numberWithDouble:659.00]],
                             [APLProduct productWithType:ProductTypeDesktop
                                                    name:@"iMac"
                                                    year:[NSNumber numberWithInteger:1997]
                                                   price:[NSNumber numberWithDouble:1299.00]],
                             [APLProduct productWithType:ProductTypeDesktop
                                                    name:@"Mac Pro"
                                                    year:[NSNumber numberWithInteger:2006]
                                                   price:[NSNumber numberWithDouble:2499.00]],
                             [APLProduct productWithType:ProductTypePortable
                                                    name:@"MacBook Air"
                                                    year:[NSNumber numberWithInteger:2008]
                                                   price:[NSNumber numberWithDouble:1799.00]],
                             [APLProduct productWithType:ProductTypePortable
                                                    name:@"MacBook Pro"
                                                    year:[NSNumber numberWithInteger:2006]
                                                   price:[NSNumber numberWithDouble:1499.00]]
                              ];

    UINavigationController *navigationController = (UINavigationController *)[self.window rootViewController];
    APLViewController *viewController = [navigationController.viewControllers objectAtIndex:0];
    viewController.products = productArray;

    return YES;
}

検索部分の実装

前出のものと違うのは検索部分だったのでその部分のみ抜粋して説明のコメントを追記。

- (void)updateFilteredContentForSearchString:(NSString *)searchString productType:(NSString *)type
{
    // start out with the entire list
    self.searchResults = [self.products mutableCopy];

    // strip out all the leading and trailing spaces
    //まず文字列の両端からスペースを取り除く
    NSString *strippedStr = [searchString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

    // break up the search terms (separated by spaces)
    NSArray *searchItems = nil;
    if (strippedStr.length > 0)
    {
        //スペースで区切って文字列をNSArrayに
        searchItems = [strippedStr componentsSeparatedByString:@" "];
    }

    // build all the "AND" expressions for each value in the searchString
    // AND条件のため要素をフィルタリングするNSPredicateを保持する
    NSMutableArray *andMatchPredicates = [NSMutableArray array];

    //スペースで区切られた文字列を列挙
    for (NSString *searchString in searchItems)
    {
        // each searchString creates an OR predicate for: name, yearIntroduced, introPrice
        //
        // example if searchItems contains "iphone 599 2007":
        //      name CONTAINS[c] "iphone"
        //      name CONTAINS[c] "599", yearIntroduced ==[c] 599, introPrice ==[c] 599
        //      name CONTAINS[c] "2007", yearIntroduced ==[c] 2007, introPrice ==[c] 2007
        //
        //文字列はnameから
        //数字は年と値段両方の条件として使われる

        NSMutableArray *searchItemsPredicate = [NSMutableArray array];

        // name field matching
        NSExpression *lhs = [NSExpression expressionForKeyPath:@"name"];
        NSExpression *rhs = [NSExpression expressionForConstantValue:searchString];

        NSPredicate *finalPredicate = [NSComparisonPredicate
                                       predicateWithLeftExpression:lhs
                                       rightExpression:rhs
                                       modifier:NSDirectPredicateModifier
                                       type:NSContainsPredicateOperatorType
                                       options:NSCaseInsensitivePredicateOption];
        [searchItemsPredicate addObject:finalPredicate];

        // yearIntroduced field matching
        NSNumberFormatter *numFormatter = [[NSNumberFormatter alloc] init];
        [numFormatter setNumberStyle:NSNumberFormatterNoStyle];
        NSNumber *targetNumber = [numFormatter numberFromString:searchString];
        if (targetNumber != nil)    // (searchString may not convert to a number)
        {

            lhs = [NSExpression expressionForKeyPath:@"yearIntroduced"];
            rhs = [NSExpression expressionForConstantValue:targetNumber];
            finalPredicate = [NSComparisonPredicate
                              predicateWithLeftExpression:lhs
                              rightExpression:rhs
                              modifier:NSDirectPredicateModifier
                              type:NSEqualToPredicateOperatorType
                              options:NSCaseInsensitivePredicateOption];
            [searchItemsPredicate addObject:finalPredicate];

            // price field matching
            lhs = [NSExpression expressionForKeyPath:@"introPrice"];
            rhs = [NSExpression expressionForConstantValue:targetNumber];
            finalPredicate = [NSComparisonPredicate
                              predicateWithLeftExpression:lhs
                              rightExpression:rhs
                              modifier:NSDirectPredicateModifier
                              type:NSEqualToPredicateOperatorType
                              options:NSCaseInsensitivePredicateOption];
            [searchItemsPredicate addObject:finalPredicate];
        }

        // at this OR predicate to our master AND predicate
        // 名前、値段、年をOR条件として作成
        NSCompoundPredicate *orMatchPredicates
            = (NSCompoundPredicate *)[NSCompoundPredicate orPredicateWithSubpredicates:searchItemsPredicate];

        //ここでaddされるものはAND条件になる? (名前 OR 値段 OR 年) AND (名前 OR 値段 OR 年)
        [andMatchPredicates addObject:orMatchPredicates];
    }

    NSCompoundPredicate *finalCompoundPredicate = nil;

    if (type != nil)
    {
        // we have a scope type to narrow our search further
        //
        if (andMatchPredicates.count > 0)
        {
            // we have a scope type and other fields to search on -
            // so match up the fields of the Product object AND its product type
            //
            NSCompoundPredicate *compPredicate1 =
                (NSCompoundPredicate *)[NSCompoundPredicate andPredicateWithSubpredicates:andMatchPredicates];

            NSPredicate *compPredicate2 = [NSPredicate predicateWithFormat:@"(SELF.type == %@)", type];

            //区分がある時は文字列の比較とNSPredicateの==であわせて比較する
            finalCompoundPredicate =
                (NSCompoundPredicate *)[NSCompoundPredicate andPredicateWithSubpredicates:@[compPredicate1, compPredicate2]];
        }
        else
        {
            // match up by product scope type only
            //区分のみのときは区分のみで比較する
            finalCompoundPredicate =
                (NSCompoundPredicate *)[NSPredicate predicateWithFormat:@"(SELF.type == %@)", type];
        }
    }
    else
    {
        // no scope type specified, just match up the fields of the Product object
        finalCompoundPredicate =
            (NSCompoundPredicate *)[NSCompoundPredicate andPredicateWithSubpredicates:andMatchPredicates];
    }

    self.searchResults = [[self.searchResults filteredArrayUsingPredicate:finalCompoundPredicate] mutableCopy];
}
yimajo
株式会社キュリオシティソフトウェアの代表です。iOSアプリを作っています。最近はCombine frameworkガイドブック / RxSwift研究読本などを書いてます。
https://swift.booth.pm/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away