Search a UITableView

Never have I used a magnifying glass to search
Never have I used a magnifying glass to search

Note: This article has been marked for a quality review and will soon be updated.

As with most things there are a bunch of tutorials on the internet about how to search a UITableView, but when I was attempting to implement my search I hit a brick wall with it and ended back where I started - Apple's developer docs - the code from which will be used in this here tutorial. I trawled through many tutorials and cannibalised a load of code, but finally I tamed the beast and got myself a lovely search bar. So this post is as much a form of catharsis for me, as it is a tutorial for you :) Let's get started.

To begin we need to think about what we need for this - we want a search bar, we need a search button, we may even need some scope titles to allow us to filter our results. Something which might not be too obvious to begin with is the fact that we also need to think about the UITableView in which to load our results. When we've finished loading this UITableView, it will display it to the user to scroll through. In terms of variables we also need to consider the fact that we will need a duplicate of our main data - be it an array or dictionary, so that we can store our search results. Lastly we need to store the search term and selected scope so that if our users switch to another view we can retain that information. All of this code is available from Apple, but I'll show you it here in a real world implementation - they use their own "Product" class, but I'll show you how to accomplish search with an NSDictionary and NSArray. Now at this point you may be wondering what you've let yourself in for, after all I've just gone through a rather long list of UI elements that are required to search our table, but luckily for us Apple provides a UISearchDisplayController class, which handles the UI, and expects only the back-end implementation to be written by us. So I'm going to create a new view-based project and get started.

My data structure will be as follows: I will have an NSArray populated with NSDictionary objects holding all my data, I will then populate my table-view with that data, and implement the search functionality based on that structure. If you want to use some other type of object, just replace all mention of NSArray etc. You should also note that Apple's implementation places code inside a view controller based on the UITableView class, not the UIView class - this will affect what variables we need, and what we need to hook up in IB. In this tutorial we will put a UITableView within a UIView.

Go ahead and add a UITableView, and then add a "Search Bar and Search Display Controller" to your UITableView, and link up the dataSource and delegate parts, making sure to implement their relevant protocols - you know how to set up a UITableView right? That handy object, as you might be able to tell, handles our search bar, and the controller for our search. We don't need to link to these expressly in our code by creating variables and properties, but we will need to implement a couple of protocols - namely the UISearchDisplayDelegate, UISearchBarDelegate ones. The search controller that we added in IB automatically sets it's delegate, so there's no need to hook that up manually. So now let's set up our variables. I'm putting these in my .h file:

NSArray*listItems;
NSMutableArray *filteredListItems;
NSString *savedSearchTerm;
NSInteger savedScopeButtonIndex;
BOOL SearchWasActive;
IBOutlet UITableView tableView; // Be sure to hook this up in IB

The role of some of these variables will soon become clear. Don't forget to link the tableView variable up in IB before continuing. Then to finish up in the .h file we need to add our properties like so:

@property (nonatomic, retain) NSArray *listItems;
@property (nonatomic, retain) NSMutableArray *filteredListItems;

@property (nonatomic, copy) NSString *savedSearchTerm;
@property (nonatomic) NSInteger savedScopeButtonIndex;
@property (nonatomic) BOOL searchWasActive;

Notice the mutability of our filtered content array - this is very important as we will be changing the content of this data store each time the user performs a search. If you want to use a dictionary at this point, you know what to do. And with that we can wave goodbye to our header, and welcome our implementation. Once you've synthesised your variables, we need to look at the methods required by the protocols we promised to implement.

The first two we will write are fairly simple, they will be called when a search is performed:

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
    [self filterContentForSearchText:searchString scope:
			[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];

    return YES;
}


- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption {
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:
			[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:searchOption]];
    return YES;
}

Those two bad-boys each call another function, and then exit while returning "YES". When a user types something in the search field the first function is called, it then passes what it knows to the function filterContentForSearchText:scope:. The second is called whenever the user selects a different scope, and does the same as the first. Great stuff, we only have 1 more function to implement our search and a few lines to put elsewhere a little later on. And this is the part that caught me out to begin with. For some reason Apple neglected to provided simple code concerned with searching something like an array, and instead they provide a separate class - maybe that's more common where they come from, but round these parts I'm rather partial to the ol' NSArray and NSDictionary. So I changed certain parts of the following code to allow the use of said objects in our table. Just so you know my dictionaries that are stored in my listItems array have the keys "title" and "type". This is what I got:

- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{	
	[filteredListItems removeAllObjects]; // Clear the filtered array.

	for (NSDictionary *item in listItems)
	{
		if ([scope isEqualToString:@"All"] || [[item objectForKey:@"type"]  isEqualToString:scope]  || scope == nil)
		{
			NSComparisonResult result = [[item objectForKey:@"title"] compare:searchText options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch) range:NSMakeRange(0, [searchText length])];
            if (result == NSOrderedSame)
			{
				[filteredListItems addObject:item];
            }
		}
	}
}

This function first clears the filtered array, and then uses fast enumeration to search through our main array for matches. If you don't know what fast enumeration is, in short it is a more efficient way of iterating of collections of data. Inside our loop we compare the scope to "All", and if we have a match we use some nifty functions to see if our search term matches what we have in our array. Notice the if statement also features two other parts - the second compares our "type" key - so maybe dinner or snack, to the scope buttons, and the last checks to see if the scope is nil - meaning we haven't set any buttons for our scope. So if we select the "Snack" scope button it will only compare items with a type set to "Snack".

This next bit of code is for your viewDidLoad method.

// create a filtered list that will contain products for the search results table.
//filteredListItems = [NSMutableArray arrayWithCapacity:[listItems count]];
filteredListItems = [[NSMutableArray alloc] initWithCapacity:[listItems count]];

// restore search settings if they were saved in didReceiveMemoryWarning.
if (self.savedSearchTerm){
    [self.searchDisplayController setActive:self.searchWasActive];
    [self.searchDisplayController.searchBar setSelectedScopeButtonIndex:self.savedScopeButtonIndex];
    [self.searchDisplayController.searchBar setText:savedSearchTerm];
                
    self.savedSearchTerm = nil;
}

[self.tableView reloadData];

Here we simply restore any data that we have retained if our user dismissed the current view, and then reload our table to reflect any changes. Now we need to update our viewDidDisappear to make sure we actually save the data we are using in the above function, so go ahead and add the following lines to your viewDidDisappear method:

self.searchWasActive = [self.searchDisplayController isActive];
self.savedSearchTerm = [self.searchDisplayController.searchBar text];
self.savedScopeButtonIndex = [self.searchDisplayController.searchBar selectedScopeButtonIndex];

And add this to your viewDidUnload method:

 self.filteredListItems = nil;

Now we need to think about our already-implemented UITableView methods - no you're not going mad, I've not written them here, but we need to change them up a bit. At the moment they all return data based on our main list called "listItems", this is okay if we need to display our table to users, but when in search mode, our functions will be returning the unfiltered data - let's get a fixin'.

The easy way to do this is to set up an if statement to detect if we are in search mode, and then return different data based on that information, that if statement looks like:

if (tableView == self.searchDisplayController.searchResultsTableView){
    // Search mode
}else {
    // Normal mode
}

So go ahead and add this to all your protocol methods; as an example your method for returning the number of rows in a section would look like:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (tableView == self.searchDisplayController.searchResultsTableView){
        return [self.filteredListItems count];
    }
    else{
        return [self.listItems count];
    }
}

Now we are ready to run it! Go ahead and hit run, and type something in the search bar, you should see a filtered list appear as you type. Now you may be getting a warning or two if you used the variable name "tableView", about the method parameters hiding your instance variables, in this case it's not a big deal, but it might be a good idea to change the name to something a little more descriptive in the future.

So what if we want to implement some scope buttons? Well now we've done all the work behind the scenes, we can jump into IB, and add some titles. So go to IB and select your search-bar. In the attributes inspector you should see a box that says "Scope titles", just above that is a check box that says "Shows Scope Bar" - check that, and use the +/- controls to add titles. Then if you want your scope bar to be visible all the time leave the check box checked, but if you want it to only appear when the search becomes active, uncheck the box. Now when you run the program you should see that a scope bar has appeared!

And with that, we have successfully added search to our app. If you have any issues with this code do let me know, and if you want the completed project use the link below.

Completed XCode 4 Project

***

If you found this particularly useful and want to share the ♥ you can donate here.

***

4 Responses

  1. M. Rushizha

    Thanks for putting this together, Tom and making it simple. I really appreciate it.

    Reply
  2. Mike

    I appreciate you putting this tutorial together. It was a big help.

    Reply
  3. Pete Neill

    This was excellent! Easily the clearest and most concise tutorial I found. Thanks a lot for posting

    Reply
  4. Jimmy

    Straight forward freat example, exactly what I needed. Thx for posting!

    Reply


~ Comments are now closed ~

Get in touch here