Though UISearchDisplayController is seemingly easy (and yes it’s easy), apart from the sample source code, there isn’t much documentation available from Apple. I won’t be posting code for this tutorial, (as most of them come from Apple’s own source code), however, the tutorial will contain code fragments that I wish to highlight and those I changed for improving the search using NSPredicate

Step 1: Create the project

If you haven’t created a project yet, create a Navigation Based Project and open the RootViewController.xib.

Step 2: Modify the XIB

Drag UISearchDisplayController to your RootViewController.xib. (It automatically does the connections for you) Drag the SearchBar that gets added onto your TableView. You need the UISearchBar to be displayed on the controller. So drag the searchbar and place it on the top of the table view. Add ScopeBars if you need any. We will discuss later how to handle the scope bar changes and how to filter text based on the selected scope.

Step 3: The Design of UISearchDisplayController

In a traditional tableviewcontroller, you normally have one master list of data which you use for all the tableview data source methods. In case of UISearchDisplayController, you have to maintain two lists. One master list and one a filtered list. The tableview data source delegates will be called by the framework when it needs to populate your tableview or the search results. So in all the methods you have to differentiate whether the data source call is for populating the filtered list or for populating the main list.

This is done by a simple two line code.

if (tableView == self.tableView) {
// normal table view population
}
if(tableView == self.searchDisplayController.searchResultsTableView){
// search view population
}

This code should be written in cellForRowAtIndexPath, didSelectRowAtIndexPath and numberOfRowsInSection.
For the complete code listing, see Apple’s sample code.

Step 3: Implement the search delegates

When the user invokes a search,the UISearchDisplayDelegate functions you implement will be called. The two main functions that you have to implement are,

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchScope:(NSInteger)searchOption
 
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchString:(NSString *)searchString

Copy the implementation from Apple’s source.In the header file, your class should implement UISearchBarDelegate and UISearchDisplayDelegate.

As the name suggests, the first function will be called when the user changes the scope button and the second function is called when he starts typing a search query. The first function is not necessary if you haven’t added any scope button in step 2.

Step 4: Filtering your data using NSPredicate

This step is the reason why I wrote this seemingly simple post. I didn’t like the Apple’s way of filtering. It doesn’t match “contains” text. I modified the filterContentForSearchText method, so as to use NSPredicate. You still need the Apple’s implementation of shouldReloadTableForSearchString and shouldReloadTableForSearchScope.
The filterContentForSearchText implementation CANNOT be copy-pasted into your application. It’s a code fragment that doesn’t compile without some changes. Points to note in the function is the use of predicate.

NSPredicate *predicate = 
[NSPredicate predicateWithFormat:@"(SELF contains[cd] %@)", searchText];

The [cd] in the predicate format means case and diatric insensitive. the verb I have used is “contains”, you can change it to beginsWith, endsWith, like and many others. The NSPredicate is a very powerful feature that can be exploited here. I don’t understand why Apple just didn’t use it :-( For more info, look at Apple’s NSPredicate documentation here

Below is the complete code listing after making the NSPredicate changes.

- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
    [self.filteredData removeAllObjects]; // First clear the filtered array.
    for (Product *product in tableData)
    {
        NSPredicate *predicate = [NSPredicate predicateWithFormat:
           @"(SELF contains[cd] %@)", searchText];
       [product.productID compare:searchText options:NSCaseInsensitiveSearch];
        BOOL resultID = [predicate evaluateWithObject:product.productID];
        BOOL resultName = [predicate evaluateWithObject:product.productName];
       if([scope isEqualToString:@"Product ID"] && resultID)
        {
            [self.filteredData addObject:product];
        }
        if([scope isEqualToString:@"Product Name"] && resultName)
        {
            [self.filteredData addObject:product];
        }
        if([scope isEqualToString:@"Any"] && (resultID || resultName))
        {
            [self.filteredData addObject:product];
        }
    }
}

With NSPredicate, you can play around with the search filters. NSPredicate is as powerful and easier to use than Regular Expressions. Try your hand at it.


Mugunth


Follow me on Twitter

  • Ted

    There are 8 ads in that short block of code :(

    • http://intensedebate.com/people/mugunthkumar mugunthkumar

      try it now… I'm sorry.

  • http://gogodocs.com Jonathan Saggau

    There are a few little details that would make this better.
    Look at -[NSArray filteredArrayUsingPredicate:(NSPredicate *)predicate] and you won't need that loop. In any case, there is no need to make a new predicate identical to the last one each time through that loop. You should probably put it outside of the loop.

    I also suggest that you consider modifying the predicate you are filtering with, rather than filtering using the if([scope isEqualToString...] bits. For example:

    if([scope isEqualToString:@"Product ID"]) {
    predicate = [NSPredicate predicateWithFormat:@"(SELF.productID contains[cd] %@)", searchText];
    }

    then the filtered array is just:

    [tableData filteredArrayUsingPredicate:predicate]

    so you won't need a loop.

    You can even drop an AND or an OR into your predicate or just use NSCompoundPredicate's various class methods to make your filter more dynamic.

    See Apple's Predicate Programming Guide for more details

  • olof

    I dont understand the line:

    [product.productID compare:searchText options:NSCaseInsensitiveSearch];

    What does that line do?

  • Anonymous

    Thank you! This is the information I’m looking for. Great work!

  • kamlesh gk

    wonderful blog….thanx…
    Also Jonathan Saggau - ur comments are much appreciated…

    the final code in the filterContentForSearchText 

    [self.filteredstaffList removeAllObjects]; // First clear the filtered array.

    NSPredicate *predicate = nil;
    if([scope isEqualToString:@"Staff Code"])
    {
    predicate = [NSPredicate predicateWithFormat:@”(SELF.staffCode contains[cd] %@)”, searchText]; 
    }
    if([scope isEqualToString:@"Staff FullName"])
    {
    predicate = [NSPredicate predicateWithFormat:@”(SELF.staffFullName contains[cd] %@)”, searchText]; 
    }

    NSArray *array = [staffList filteredArrayUsingPredicate:predicate];
    self.filteredstaffList = [NSMutableArray arrayWithArray: array];

  • antucks

    Mugunth, thank you for introducing me the NSPredicate, yes powerful and easy to use – you just don’t know how much it helped my app!

  • Umair Aamir

    Hi…can you please tell me what is the difference between

    contains[cd] and contains[c] simple contains

    • iSeeker

      Hi, [c] means you want a case insensitive check, whereas [cd] means to make it case insensitive and diacritic insensitive.