Tuesday, July 10, 2012

UITableView How-To: Part 2 - Search

Part 1 | Part 3 | Part 4 | Part 5Part 6

 

If you have a lengthy list of data, you should provide the ability for your users to search through that data. Starting with OS 3.0, Apple made integrating search into table views easy enough that there really isn't a good reason not to include it.

 

This post is based heavily, if not entirely, on Apple's sample project called TableSearch.

 

UISearchDisplayController

 

The search UI that Apple provides is basically a table view with a search bar. They provide some pretty animations, such as sliding the search bar up to cover up the navigation bar (handy for maximizing available space, especially in landscape). The search table view is overlaid onto your existing view, so both your original table and the search table need data. The principles of delegate and data source as discussed in the last post still apply.

 

In addition to creating the controller itself, Apple also gave UIViewController a searchDisplayController property, which makes it easier to access. This obviously isn't populated by default, but be aware that it exists.

 

 

Data

 

Say you have a list of 10 items. The user performs a search, and only 2 items meet the criteria. Now think back to our datasource and delegate methods. We are answering a lot of questions about the table: how many sections, how many rows, what cell should be displayed (and what should the cell contain)? We still have to answer those questions, only now there are two tables involved. Even if there was only a single table, something would still have to be done with the data. There are two basic approaches you can take: 1) Remove items that don't match, or 2) Create a whole new list containing only items that do match. If you go with #1, you need some way of restoring the full list. So really, in either case, you are talking about 2 sources of data: the full list, and the search results. Since a separate table is involved for the search results, #2 probably makes more sense, and is what Apple shows in their demo.

 

Step Up To The Bar

 

We'll start with the easiest part of this project. Open up the view controller XIB in Interface Builder, and make sure you can see the table view. Find the Search Bar And Search Display Controller item in the library.

 

 

Do note that this is a separate choice from the standalone search bar. You can certainly roll your own solution using just the bar, but the controller is what makes the work relatively easy. So be sure to grab the one with the little orange circle.

 

In order to get the search bar to scroll with the table, we are going to add it as the table's header. Grab the library item, and drag to the upper portion of the table view. You should see a blue highlight, which is your confirmation that you will get the header.

 

 

A number of things happen automatically when you do this. Let's take a quick look at the object list:

 

 

Even though we didn't directly place it there, the Search Display Controller has been added. If we inspect the connections...

 

 

…we see a whole host of additions. The searchDisplayController property on the UIViewController has been populated. The necessary delegate and datasource connections have been made for the search controller, and the delegate has also been specified for the search bar itself. Not bad for one drag-n-drop operation. We are now done with IB, the rest is handled in code.

 

Changes to .h

 

Here is the end result, then I'll explain what's going on.

 

//  SampleViewController.h

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UISearchDisplayDelegate, UISearchBarDelegate>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) NSMutableArray *contentsList;
@property (nonatomic, retain) NSMutableArray *searchResults;
@property (nonatomic, copy) NSString *savedSearchTerm;

- (void)handleSearchForTerm:(NSString *)searchTerm;

@end

 

First, we conform to the UISearchDisplayDelegate and UISearchBarDelegate protocols. Same idea as what we did for the table view previously.

 

Next we declare a couple of new instance variables and properties. The searchResults array will hold items that match the search criteria. The savedSearchTerm is something that Apple shows in their sample, and they use it to restore the search when returning to this screen.

 

Finally, we declare a method that will do the grunt work of searching through the data.

 

Changes to .m

 

We'll start at the top and work our way down. First, synthesize properties and handle memory management duties.

 

@implementation SampleViewController

@synthesize mainTableView = ivMainTableView;
@synthesize contentsList = ivContentsList;
@synthesize searchResults = ivSearchResults;
@synthesize savedSearchTerm = ivSavedSearchTerm;

- (void)dealloc
{
[self setMainTableView:nil];
[self setContentsList:nil];
[self setSearchResults:nil];
[self setSavedSearchTerm:nil];

[super dealloc];
}

- (void)viewDidUnload
{
[super viewDidUnload];

// Save the state of the search UI so that it can be restored if the view is re-created.
[self setSavedSearchTerm:[[[self searchDisplayController] searchBar] text]];

[self setSearchResults:nil];
}

 

That last bit is from Apple's sample. The counterpart is here:

 

- (void)viewDidLoad
{
[super viewDidLoad];

...

// Restore search term
if ([self savedSearchTerm])
{
[[[self searchDisplayController] searchBar] setText:[self savedSearchTerm]];
}
}

 

Saving/restoring the search criteria, nothing fancy. Then we get to the search routine itself:

 

- (void)handleSearchForTerm:(NSString *)searchTerm
{
[self setSavedSearchTerm:searchTerm];

if ([self searchResults] == nil)
{
NSMutableArray *array = [[NSMutableArray alloc] init];
[self setSearchResults:array];
[array release], array = nil;
}

[[self searchResults] removeAllObjects];

if ([[self savedSearchTerm] length] != 0)
{
for (NSString *currentString in [self contentsList])
{
if ([currentString rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[[self searchResults] addObject:currentString];
}
}
}
}

 

First we store the search term. Then we lazily create the searchResults array if needed. Next we clear out any previous search results. Then we loop through our main data, find any matching items, and add them to the searchResults array.

 

This method could vary greatly depending on what kind of data you are working with, and how it is arranged. This particular implementation is based on an example in the Mark/LaMarche book.

 

Now we get to the fun part: the table datasource and delegate methods. We've discussed a bit already about the need to have 2 separate data lists, and pointed out that we will be dealing with 2 separate tables. However, we only have one set of delegate methods here in the controller. We could use a completely separate object, as mentioned in the previous post, but that's not really necessary. So, we need some way of determining which set of data to use. You might get away with using some kind of flag, say BOOL isCurrentlySearching or something along those lines. I have fought enough battles with the search display controller in attempting to do other tasks to know that this approach won't work. Thus, we'll go with the way Apple's sample shows, and that is to make decisions based on which table is asking for information. In hindsight, this is a really obvious approach, but I'm not always on the ball as quickly as I should be.

 

First, the number of rows:

 

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
NSInteger rows;

if (tableView == [[self searchDisplayController] searchResultsTableView])
rows = [[self searchResults] count];
else
rows = [[self contentsList] count];

return rows;
}

 

The important thing to realize here is that all of these table view delegate methods include the calling table view itself as a parameter. This allows you the means to identify which table is making the request. If you had a reason to design a view with multiple tables, this is exactly what you would do. If the table asking for info is the search table, we provide an answer based on the search list, otherwise we use our main list.

 

We do the exact same thing when providing a cell:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger row = [indexPath row];
NSString *contentForThisRow = nil;

if (tableView == [[self searchDisplayController] searchResultsTableView])
contentForThisRow = [[self searchResults] objectAtIndex:row];
else
contentForThisRow = [[self contentsList] objectAtIndex:row];

static NSString *CellIdentifier = @"CellIdentifier";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}

[[cell textLabel] setText:contentForThisRow];

return cell;
}

 

Constructing the cell itself doesn't need to change just because we're using a search table. The key thing is to make sure we're grabbing the right piece of information to populate that cell. "Green" might be at row 4 in our main list, but it could be at row 2 in the search results. Once again, we make a decision based on which table is asking, and grab data from the appropriate list.

 

I'm not really going into cell selection yet, but you would need to do something similar in tableView:didSelectRowAtIndexPath:. The index path can, and most likely will, be different depending on which table is being shown.

 

At last, we reach the part that makes this all work. We've been dealing with table delegate methods up to this point, but the search controller has delegate methods of its own. So we'll utilize a couple of those to make the actual search happen. First, to begin the search:

 

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchString:(NSString *)searchString
{
[self handleSearchForTerm:searchString];

return YES;
}

 

And that's it. The documentation notes this is an optional method, and NOT implementing it will cause the search table to reload as the search term changes. So the only reason we're doing this is to define what logic should be performed in response to the search string. And this next part is also optional, but if you want to do any cleanup after the search, this is where you could do it:

 

- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
[self setSavedSearchTerm:nil];

[[self mainTableView] reloadData];
}

 

We no longer need the search term to be saved, so get rid of it. And maybe it is appropriate to refresh the main table view.

 

That's pretty much all there is to it at a basic level. Obviously this is a simple app so far, but the search display controller handles a lot duties on its own. Here is the sample project for everything so far:

 

TableViewTutorial_Part2.zip

4 comments:

Anonymous said...

Thanks ! :)

jordanclark1993 said...

Any idea how to narrow down the detail text and an image view to match the narrowed down main text?

BriTer Ideas said...

Well, I'm not really sure what you mean by that. But if you're saying that your details are in a separate array, then you should read this:

http://briterideas.blogspot.com/2012/09/a-model-object-is-beautiful-thing.html

Monia said...

thank you so much for this tutorial, it works perfectly with sqlite3 for me :)

Post a Comment