Filtering a Grouped UITableView with a Search Bar

I’ve mentioned before that my post on Filtering a UITableView with a Search Bar has been by far my most popular post. A natural extension of that post – and something I’ve had a few questions about – is how to do the same thing for a grouped UITableView.

This post will demonstrate filtering a food list that is grouped by the first letter of each food’s name.

So let’s start out with the same sample application used in the original post. We’ll be storing our filtered data in a dictionary, with the keys being letters and the values being NSArrays containing all of the foods for each letter. We’ll also keep an array of our dictionary’s keys, sorted alphabetically. This is needed because NSDictionary is an unordered collection, and without it, our groups wouldn’t be in alphabetical order.

First, we’ll define the data structures we’ll be using in our header file.

@property (strong, nonatomic) NSArray* allTableData;
@property (strong, nonatomic) NSMutableDictionary* filteredTableData;
@property (strong, nonatomic) NSMutableArray* letters;

Next, we’ll have to implement our usual table view data source methods, but this time, we’ll be reading the values to return from our dictionary. For numberOfSectionsInTableView, we’ll return the number of items in our dictionary, and for numberOfRowsInSection, we’ll return the number of items in each section’s array (remember, our dictionary one array per section/letter).

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of letters in our letter array. Each letter represents a section.
    return letters.count;
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{    
    // Returns the number of items in the array associated with the letter for this section.
    NSString* letter = [letters objectAtIndex:section];
    NSArray* arrayForLetter = (NSArray*)[filteredTableData objectForKey:letter];
    return arrayForLetter.count;
}

We’ll also need to implement one additional method – titleForHeaderInSection. It will return the key corresponding to the specified section.

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    // Each letter is a section title
    NSString* key = [letters objectAtIndex:section];
    return key;
}

Our cellForRowAtIndexPath method has a few little changes. It has to look up the NSArray corresponding to the specified section, then find the Food object inside of the NSArray. So it now looks something like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
 
    // Grab the object for the specified row and section
    NSString* letter = [letters objectAtIndex:indexPath.section];
    NSArray* arrayForLetter = (NSArray*)[filteredTableData objectForKey:letter];
    Food* food = (Food*)[arrayForLetter objectAtIndex:indexPath.row];
 
    cell.textLabel.text = food.name;
    cell.detailTextLabel.text = food.description;
 
    return cell;
}

That’s all well and good, but how do we actually set up our dictionary? How do we actually, you know, filter stuff?

The updateTableData method takes care of this. This method loops through each Food object to see if it matches our search string. If it does, the code checks to see if there is an existing NSMutableArray for its first letter. If so, the Food object is added to the existing array. if not, a new NSMutableArray is created and added to the dictionary, and the Food object is added to it.

-(void)updateTableData:(NSString*)searchString
{
    filteredTableData = [[NSMutableDictionary alloc] init];
 
    for (Food* food in allTableData)
    {
        bool isMatch = false;
        if(searchString.length == 0)
        {
            // If our search string is empty, everything is a match
            isMatch = true;
        }
        else
        {
            // If we have a search string, check to see if it matches the food's name or description
            NSRange nameRange = [food.name rangeOfString:searchString options:NSCaseInsensitiveSearch];
            NSRange descriptionRange = [food.description rangeOfString:searchString options:NSCaseInsensitiveSearch];
            if(nameRange.location != NSNotFound || descriptionRange.location != NSNotFound)
                isMatch = true;
        }
 
        // If we have a match...
        if(isMatch)
        {
            // Find the first letter of the food's name. This will be its gropu
            NSString* firstLetter = [food.name substringToIndex:1];
 
            // Check to see if we already have an array for this group
            NSMutableArray* arrayForLetter = (NSMutableArray*)[filteredTableData objectForKey:firstLetter];
            if(arrayForLetter == nil)
            {
                // If we don't, create one, and add it to our dictionary
                arrayForLetter = [[NSMutableArray alloc] init];
                [filteredTableData setValue:arrayForLetter forKey:firstLetter];
            }
 
            // Finally, add the food to this group's array
            [arrayForLetter addObject:food];
        }         
    }
 
    // Make a copy of our dictionary's keys, and sort them
    letters = [[filteredTableData allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
 
    // Finally, refresh the table
    [self.tableView reloadData];
}

We’ll also need to take care of a few little details, such as setting up our initial data structures, and hooking up our updateTableData method to the search bar.

 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    searchBar.delegate = (id)self;
 
    allTableData = [[NSArray alloc] initWithObjects:
        [[Food alloc] initWithName:@"Pizza" andDescription:@"Pepperonni"], 
        [[Food alloc] initWithName:@"Fruit" andDescription:@"Apple"], 
        [[Food alloc] initWithName:@"Apple" andDescription:@"Red"], 
        [[Food alloc] initWithName:@"Apple" andDescription:@"Green"], 
        [[Food alloc] initWithName:@"Bread" andDescription:@"Brown"], 
        [[Food alloc] initWithName:@"Hot Dog" andDescription:@"Beef"], 
        [[Food alloc] initWithName:@"Hot Dog" andDescription:@"Chicken"],
        [[Food alloc] initWithName:@"Potato" andDescription:@"Baked"], 
        [[Food alloc] initWithName:@"Potato" andDescription:@"Mashed"], 
        [[Food alloc] initWithName:@"Hamburger" andDescription:@"Beef"], 
        [[Food alloc] initWithName:@"Steak" andDescription:@"Rare"], 
        [[Food alloc] initWithName:@"Steak" andDescription:@"Medium"], 
        [[Food alloc] initWithName:@"Salad" andDescription:@"Caesar"], 
        [[Food alloc] initWithName:@"Salad" andDescription:@"Bean"], 
        nil ];
 
    [self updateTableData:@""];
}
 
-(void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)text
{  
    [self updateTableData:text];
}

Once all that is complete, we’ll have a filterable, grouped UITableView. Enjoy! As a side note, I’m sure there are plenty of other ways to accomplish this task, but this method is relatively simple and it seems to do the trick.

Download The Sample Project

5 thoughts on “Filtering a Grouped UITableView with a Search Bar

  1. Very fine Tutorial.
    Short, elegant, easy

    Do you can also show a way how to search a dictionary with key values?
    I tried but I’m a newcomer in Xcode and it seems what i wish is still a little to heavy for me.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>