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:
- The user taps the
UISearchBar
.UISearchDisplayController
activates the search interface, brings up the keyboard. It creates a table view for the results. - The user enters text to search for.
UISearchDisplayController
informs theUISearchDisplayDelegate
of this. TheUISearchDisplayController
asks the delegate if it should reload its table (the default is YES). - When the
UISearchDisplayController
wants to reload the table, it asks theUITableViewDataSource
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. - If a user selects one of the rows in the table view owned by the
UISearchDisplayController
, it lets theUITableViewDelegate
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:
It's for iPhone OS 3.2 and above only.