The ingredients for a search function in a UITableView are more involved than just displaying a simple search field. First we need the standard UITableView. Next we need a UISearchBar which can be added to the top of the table view. But displaying the actual search results is something called the UISearchDisplayController.
This process isn’t very well documented from what I could find, so here’s how I did it successfully. Works fine in iOS 6.
For iOS 5 compatibility please see my comment at the end.
This new controller “switches out” the standard table view and overlays a new one (the search display). Since the data for the search view is more or less the same as what was already powering the original table view (only “filtered”), the search view often uses the same data source as the original table view.
Speaking of filtering the original data: this is done with something called a predicate (NSPredicate). First time I’ve heard of this was in regards to Core Data. A predicate is a rather powerful yet mysterious thing and rest assured you don’t need to understand it in order to use it.
Let’s go through this step by step: from creating a standard table view with some dummy data, then adding a search function and slowly making it work.
Creating the Table View
I’ll start with a Single View Application project (with Storyboards and ARC selected), removing the single View Controller from the Storyboard. Delete both ViewController h and m files too.
Next we’ll drag a UITableViewController into the Storyboard. Select the cell and set its reuse identifier to “Cell”. Next we’ll create a new Cocoa Touch Objective-C Class called TableViewController, naturally being a sub class of UITableViewController. Before we do anything else, let’s set this custom class in our Storyboard.
We’ll also create two properties in our h file:
- an NSArray called allData, holding all our data
- an NSMutableArray called searchResults which will hold our filtered results
Before we forget, let’s initialize the latter in our viewDidLoad method:
self.searchResults = [[NSMutableArray alloc]init];
Creating some Dummy Data
Here’s a small method that initializes our data: 24 written out numbers. This could be anything really:
- (void)createData { self.allData = [[NSArray alloc]initWithObjects:@"One", @"Two", @"Three", @"Four", @"Five", @"Six", @"Seven", @"Eight", @"Nine", @"Ten", @"Eleven", @"Twelve", @"Thirteen", @"Fourteen", @"Fifteen", @"Sixteen", @"Seventeen", @"Eighteen", @"Ninteteen", @"Twenty", @"Twentyone", @"Twentytwo", @"Twentythree", @"Twentyfour", nil]; }
Let’s call this method in our viewDidLoad so it’s ready for use as soon as the app starts:
[self createData];
Populating the Table View
Let’s make our table view show those items: take out the two #warnings and all commented code to make our class a bit more readable. Next set the tableView:numberOfSections to 1 as we wont be dealing with sections here.
The tableView:numberOfRowsInSection needs to be the amount of our allData array, so we’ll make it look like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return self.allData.count; }
And the final method we need to amend is the tableview:cellForRowAtIndexPath method so that text gets displayed in each cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; // Configure the cell... cell.textLabel.text = [self.allData objectAtIndex:indexPath.row]; return cell; }
Note that since Xcode 4.6 the template for a new Table View class has changed to incorporate an extremely annoying addition: right after the line with dequeueReusableCellWithIdentifier, we find “forIndexPath:indexPath”. This makes your app crash in iOS 5 and sometimes in iOS 6. Remove it to reflect my code above – you’ll avoid a lot of hair pulling later!
Run the app to see all our values listed as expected.
We’re not dealing with selecting them, but we’ll see next how we can make the view change into search mode.
Bring on the big Search Bar
This is where things get interesting: back in the storyboard, drag a UISearchDisplayController to the very top of your table view, so that it sits above the dummy cell. Switch to Assistant Editor mode and control drag a reference into your h file so we can address this thing.
Notice that when you do this you will only be able to create an outlet to a UISearchBar – that’s fine, it’s part of the UISearchDisplayController. Call it searchBar.
Setting up our Predicate
Back in the TableViewController.m file we need to add a couple of helper methods. The first one is a method called filterData. We will call this repeatedly so a small method will work best for us:
- (void)filterData { [self.searchResults removeAllObjects]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self CONTAINS [cd] %@", self.searchBar.text]; self.searchResults = [[self.allData filteredArrayUsingPredicate:predicate] mutableCopy]; }
This method will take what’s in our search bar and compare it to our allData array. Matching items are placed into our searchResults array (which we clear out at the beginning of this method). The [cd] statement means we don’t want to know about caSe sEnSitiVity or “diacritics” which are things like the Umlaut Dots above a letter and such.
The great thing for us is that after calling this method, we know how many matches we have and hence how many cells we need to display. We want to call this method every time someone adds a single letter to our search bar. And in order to know when that is, we better conform to the
UISearchBar Delegate Protocol
If we’re the Search Bar’s delegate, then we can implement a method that tells us when our search text changed. This is what we want! Head over to your TableViewController.h file and add the protocol declaration to the top:
@interface TableViewController : UITableViewController <UISearchBarDelegate>
Back in the m file add this method, which in turn will call our filterData method:
- (void) searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { [self filterData]; }
There’s no need to setup the delegate; since we’ve implemented a Search Display Controller, this has been done for us already.
Dealing with the Search Display Controller
This controller is rather clever: rather than bringing up a modal view or anything visually disruptive, the Search Display Controller “overlays” his own table view above out existing one, dimming the latter out. It’s slick alright – but therefore also a tad confusing. Because out of a sudden, we have TWO table views to deal with.
So when we tell the usual data source methods how many sections, rows and cells we need to display, we need to do so via an if-then statement so see which table view is requesting the data.
We’ll start with the tableView:numberOfRowsInSection method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. if (tableView == self.tableView) { return self.allData.count; } [self filterData]; return self.searchResults.count; }
Here we ask: if it’s the “normal” table view, show the number of our allData array. If that’s not the case, we’ll be asked by the Search Display Controller. In which case, call the filterData method, then see how many items are, and then count those.
We need to do the same for our cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (!cell) { cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"]; } if (tableView == self.tableView) { // Configure the cell... cell.textLabel.text = [self.allData objectAtIndex:indexPath.row]; } else { cell.textLabel.text = [self.searchResults objectAtIndex:indexPath.row]; } return cell; }
Again if the “standard” table view is asking, display an object from the allData. If it’s the other guy, display it from searchResults (by which time we would have just called the filterData method so we’re not calling it again).
One other point of note here is the if(!cell) statement:
Leaving this out makes the table view crash as soon as you hit a letter. For some reason cells don’t automatically create themselves as needed, which works fine in a standard table view. So if there aren’t any, we just create as many as needed.
Full Project Code
You can get the entire project on my repository on GitHub: Table Search 2013
Further Reading
- TableSearch – Apple’s Demo Code, last updated in 2010 and features iOS 4, does not make use of predicates (at the time of writing)
- Predicates Programming Guide
- UISearchDisplayController Class Reference