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

A few more Default.PNGs for the archive

Here is my archive of Default.png and Default@2x.png images. There are currently three different layouts. I will be adding to the collection as I create different ones for my apps. The archive contains regular images and @2x images.

Download the Default.png archive

A UITableView with a navigation controller:

 

A grouped UITableView with a navigation controller

A UITableView with a navigation controller and a tab bar:

 

Feel free to use these images in your apps. Enjoy!

Creating NSManagedObject Subclasses – So Handy, Yet So Strange

If you are a Core Data user, you’ll likely become intimately familiar with NSManagedObject and the Core Data model editor.

One aspect of NSManagedObject is the the creation of NSManagedObject subclasses. The only way I have found to (automatically) do this is to select your entities, then go to Editor -> Create NSManagedObjectSubclass.

Why on earth is ‘Create NSManagedObject Subclass’ a global menu item? And why is it in the ‘Editor’ menu? Whose brilliant idea was it to have a global menu item for something so incredibly specific to a particular portion of a particular screen? Why is generating code from your Core Data model so completely non-discoverable? Why isn’t there a ‘Create NSManagedObject Subclass’ button right next to the ‘Add Entity’ button?

And those are just my issues with the placement of the functionality. So aside from that, how well does it actually work?

In my limited usage so far, I would say that it’s marginally useable.

First of all, having to perform a manual process in order to keep your data model and your object model in sync is pointless. By contrast, when you edit an Entity Framework EDMX file or a LINQ to SQL DBML file, your object model classes are automatically regenerated. And hey, if you’re using EF Code-First, there’s nothing else you need to do!

Second, the code generation itself seems… inflexible. As far as I can tell, there is absolutely nothing remotely similar to .TT files in Xcode/Core Data. For those unfamiliar with the Visual Studio world, a .TT file is essentially a template that can describe, among other things, how classes are generated from a data model. You can customize a .TT file so that the generated classes look exactly like you want them to – add in validation attributes, extra fields, property change notifications, and so on. This standard functionality in EF/VS, but doesn’t seem to exist in Xcode.

In order to something similar, it appears that you need a third-party tool such as mogenerator. Now it’s great that such a tool exists, but it’s sad that it needs to exist. Customization of generated code is a pretty basic IDE feature these days, and it’s yet another area where Xcode falls short of the competition.

On another code generation note, it’s quite amusing that Xcode can’t even remember what group I previously added the NSManagedObject subclasses to, forcing me to manually select it every time I change my data model.

iOS App Postmortems – Which Episode?

Which Episode? is now available on the app store, and that means it’s time for another postmortem.

Development Time:

  • ~8h

Review Time (time spent in the iOS app submission queue):

  • 9 days

New technologies I learned and used:

  • The only real new thing here was iOS 5’s handy NSJSONSerialization class, which is used to look up your TV shows and download posters form them.

Challenges:

  • The biggest problem here was figuring out where to get posters of TV shows. After initially settling on http://www.imdbapi.com/, I became unhappy with the (movie-centric) quality of the results, and the inability to retrieve more than one image in case of multiple shows with the same name.
    So after looking around for quite some time, and being continuously disappointed in either the licensing or the technology of each particular solution, I stumbled across http://trakt.tv and their ridiculously awesome API. Problem solved!

Future plans: The main change that will likely be coming in the future is the ability to browse between different images for your shows, just in case the default image is for the wrong show. For an example of where this would be handy, try searching for ‘Angel’. You won’t get a show about vampires.

Aside from that, the application does everything I want it do do, and aside from user requests, it likely won’t get too many more features. So as always, please let me know if there’s anything you would like to see in it!

iOS Pain Points – The Prevalence of Magic Strings

One thing I’ve noticed while working with iOS is that magic strings seem to be quite common.
They’re in Core Data:

[NSEntityDescription insertNewObjectForEntityForName:@"Foo" inManagedObjectContext:context];

They’re in Storyboards and View Controllers:

[self.storyboard instantiateViewControllerWithIdentifier:@"SomeViewControllerIdentifier"];

They’re in Segues:

[self performSegueWithIdentifier:@"SomeSegueIdentifier" sender:self];

They’re in sorting:

NSSortDescriptor* sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name"
                                           ascending:YES];

They’re in the instantiation of table cells:

static NSString* CellIdentifier = @"Cell";
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

Coming from the C# world of lambdas, generics, first-class enums, and strong typing, I find all of this incredibly bizzare. But hey, who needs refactorability and type safety when you can use strings!

iOS Quick Tip – Core Data and “unrecognized selector sent to instance 0xsomething”

While experimenting with Core Data, I ran into a little issue with the always annoying ‘unrecognized selector’ exception.
I had created my Core Data model, along with my NSManagedObjectContext classes, and I was trying to use them like this:

Foo* foo = [[Foo alloc] init];
foo.bar = @"This line will crash!";

… which of course results in the aforementioned exception.

This is what happens when you don’t read the documentation. It quite clearly states that you pretty much have to use NSEntityDescription’s insertNewObjectForEntityForName:inManagedObjectContext method to create your objects.

And sure enough, if you create your objects like this instead:

Foo* foo = [NSEntityDescription insertNewObjectForEntityForName:@"Foo" inManagedObjectContext:context];
foo.summary = @"This is a bug";

it works as expected.

That said, making a class called ‘NSEntityDescription’ responsible for creating objects and adding them to a context seems like a somewhat dubious design decision.