Patrick Crosby's Internet Presents

iPhone/iPad UISearchBar and UISearchDisplayController Asynchronous Example

27 Apr 2010

All of the Apple examples and almost everything I could find on the net showing how to use UISearchBar and UISearchDisplayController operated on an existing set of data in a data structure in the application. I want to search either a database locally on the iPhone/iPad or make a remote search request to an internet API. Here are my notes on how to get this working.

First it is necessary to understand the elements involved. UISearchBar is the user interface widget. The bar with the text field with rounded sides. You can use this by itself and connect its delegate to something that implements UISearchBarDelegate.

But, beginning with iPhone OS 3.0, there is a purportedly better way: UISearchDisplayController. There's no need to subclass it, just use it out of the box. It is the glue between various parts of the search process. You initialize it with a UISearchBar and a "contentsController". While the Apple docs state that the contents controller is usually a UITableViewController, it doesn't have to be, and I think it's simpler to understand what's going on if you just use a UIViewController.

The UISearchDisplayController becomes the delegate for the UISearchBar. You need to wire up a few more connections: a UISearchDisplayDelegate, a UITableViewDataSource that provides the search result data, and a UITableViewDelegate for handling selecting the search result table cells.

Here's what UISearchDisplayController does in a typical search scenario:

  1. The user taps the UISearchBar. UISearchDisplayController activates the search interface, brings up the keyboard. It creates a table view for the results.
  2. The user enters text to search for. UISearchDisplayController informs the UISearchDisplayDelegate of this. The UISearchDisplayController asks the delegate if it should reload its table (the default is YES).
  3. When the UISearchDisplayController wants to reload the table, it asks the UITableViewDataSource for the number of rows and the cells for each row it wants. It displays the results table view on top of the existing controller.
  4. If a user selects one of the rows in the table view owned by the UISearchDisplayController, it lets the UITableViewDelegate handle it.

The key point (at least for me) to understand here is that UISearchDisplayController creates its own UITableView. It puts it on top of the contentsController when there are search results. With all the examples I found, the contentsController was itself a UITableViewController and had its own UITableView that it was displaying.

The basic examples all work like this: there's a UITableViewController with 200 rows of data. There's a UISearchBar and a UISearchDisplayController. All of the delegates for the UISearchDisplayController connect to the UITableViewController. When the search is done, the UISearchDisplayController overlays its table with a filtered set of the original rows on top of the existing table of 200 rows. When the search interface is dismissed, the overlay table is removed and the original table is available.

Clearly these basic examples don't work for a large database or an internet search API. You can't load all the results into a table data source before the search and just filter them for the search results. You need to make a request for the search results, and to keep your app responsive, you should make that request asynchronously.

Apple hints at how to do this in their documentation for UISearchDisplayDelegate in the searchDisplayController:shouldReloadTableForSearchString: and searchDisplayController:shouldReloadTableForSearchScope:.

You might implement this method if you want to perform an asynchronous search. You would initiate the search in this method, then return NO. You would reload the table when you have results.

For this example, I'll create a new project in xcode, an iPad Split-view based application. Open MainWindow.xib in Interface Builder, remove the RootViewController. It's a table view controller and doesn't need to be for this example. Drag a standard view controller into its place.

Back in xcode, go ahead and trash RootViewController.m and RootViewController.h. Remove references to it in the app delegate and the details controller. Add a new file, a UIViewController subclass with "Targeted for iPad" checked. Name it GenericViewController.

In interface builder, change the class of the view controller you added to GenericViewController. Add a view to GenericViewController. Drag the "Search Bar and Search Display Controller" to fit underneath the view. Doing this in Interface Builder makes it automatically connect the search display controller to the GenericViewController for all its delegates. This is fine for this example, but feel free to change this wiring if you want.

You can build it now, it will compile without errors and run. But if you try to search for anything, it will crash. This is because GenericViewController is set to be the delegate for all kinds of things for the search display controller, but none of the required protocols are implemented. Start by adding the protocols to the interface in GenericViewController.h:

@interface GenericViewController : UIViewController <UISearchDisplayDelegate, UITableViewDataSource, UITableViewDelegate> {

}

And in the implementation, paste this in for now (copied from the RootViewController default):

- (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView {
        // Return the number of sections.
        return 1;
}

- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
        // Return the number of rows in the section.
        return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

        static NSString *CellIdentifier = @"CellIdentifier";

        // Dequeue or create a cell of the appropriate type.
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
        if (cell == nil) {
                cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
                cell.accessoryType = UITableViewCellAccessoryNone;
        }

        // Configure the cell.
        cell.textLabel.text = [NSString stringWithFormat:@"Row %d", indexPath.row];
        return cell;
}

Now you can build it and when you search, it always returns the same "Row 1 - 10" data, but you can see UISearchDisplayController doing its work and overlaying a table view of results.

Enough synchronous stuff. Start the asynchronicity! For the purposes of this demo, we're going to fake a slow search by using an NSTimer. Here's the crucial method to add to GenericViewController:

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
        [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(mockSearch:) userInfo:searchString repeats:NO];
        return NO;
}

By returning NO, the search display controller won't reload its data. The timer calls mockSearch after a two second delay. Here's mockSearch:

- (void)mockSearch:(NSTimer*)timer
{
        [_data removeAllObjects];
        int count = 1 + random() % 20;
        for (int i = 0; i < count; i++) {
                [_data addObject:timer.userInfo];
        }
        [self.searchDisplayController.searchResultsTableView reloadData];
}

The _data variable is an NSMutableArray member variable. It shoves a random number of rows containing the search string (in userInfo) into the array, then tells the search display controller's table view to reload its data. I changed these methods to use _data for the rows:

- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
        return [_data count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

        static NSString *CellIdentifier = @"CellIdentifier";

        // Dequeue or create a cell of the appropriate type.
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
        if (cell == nil) {
                cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
                cell.accessoryType = UITableViewCellAccessoryNone;
        }

        // Configure the cell.
        cell.textLabel.text = [NSString stringWithFormat:@"Row %d: %@", indexPath.row, [_data objectAtIndex:indexPath.row]];
        return cell;
}

So that's about it. To use it in the real world, change the NSTimer call to some asynchronous network or database call. When it is done, reload the results table just like in mockSearch. One nice thing would be to also implement searchDisplayControllerDidBeginSearch: and searchDisplayControllerDidEndSearch: to display some sort of "Loading..." view on top of the results view until the results come in.

Update: By popular demand, here is the full source and xcode project for this example:

SearchExample.tar.gz

It's for iPhone OS 3.2 and above only.