Implementing UITableView Sections from an NSArray of NSDictionary Objects

Screen shot 2010-12-10 at 3.02.09 PM

If you’re working with a remote Web Service, your apps are probably displaying TableViews of objects. As soon as your dataset grows beyond 20 or 30 objects, it’s time to implement sections in your Table View. I’m going to show you how you can do this without too much trouble. In this example, we’ll use an array of dictionary objects (Books) to construct a single ‘sections’ dictionary that will be the basis for our TableView datasource.

Before we get started, you might want to clone the git repo containing the demo Xcode project for this post:

git clone [email protected]:elc/ICB_SectionedTableViewDemo.git

First let’s take a look at the UITableViewDataSource protocol methods that we’ll be using to get sections going:

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {

These are the four methods needed to show section headers, as well as the section index (which is the scrubber on the right side). Normally, with a established dataset (say one that already contains at least one item that starts with each letter of the alphabet), you can take some short cuts and return 26 for numberOfSectionsInTableView, hard code an array of A-Z to use for titleForHeaderInSection, etc. I’ve done that a few times for static datasets, but it’s really the wrong solution if you’re pulling data from a user-generated or otherwise dynamic datastore.

Instead what we’ll do is build a new dictionary entirely. The keys of this dictionary will correspond to our tableview section titles, and the values for those keys will be arrays containing our dictionary objects; these represent cells in the tableview.

Let’s start by establishing our sections:


    BOOL found;

    // Loop through the books and create our keys
    for (NSDictionary *book in self.books)
    {
        NSString *c = [[book objectForKey:@"title"] substringToIndex:1];

        found = NO;

        for (NSString *str in [self.sections allKeys])
        {
            if ([str isEqualToString:c])
            {
                found = YES;
            }
        }

        if (!found)
        {
            [self.sections setValue:[[NSMutableArray alloc] init] forKey:c];
        }
    }

If you’re implementing this code as you go, make sure you’ve setup ‘sections’ as a NSMutableDictionary property of your datasource (which is usually just your UITableViewController). So after this block of code runs, you’ll have a dictionary with a key for each unique first character of book->title. So if your books are:

On Intelligence
On The Road
Ishmael
Dune

Your self.sections will be:

‘O’ => empty NSMutableArray
‘I’ => empty NSMutableArray
‘D’ => empty NSMutableArray

Notice that the keys in your NSDictionary aren’t sorted alphabetically. They would be if your initial datasource (self.books) was sorted alphabetically; but there’s no guarantee on that. I’ll show you how to deal with this in a minute.

So the next step is to populate the empty arrays in self.sections with the appropriate NSDictionary objects from self.books:

    // Loop again and sort the books into their respective keys
    for (NSDictionary *book in self.books)
    {
        [[self.sections objectForKey:[[book objectForKey:@"title"] substringToIndex:1]] addObject:book];
    }

Now the books are compartmentalized into their respective sections. Next we need to sort the books within each section:

    // Sort each section array
    for (NSString *key in [self.sections allKeys])
    {
        [[self.sections objectForKey:key] sortUsingDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES]]];
    }

Again, we’re basing our sort off @”title”. I’ll leave @”title” in here for readability, but it would be wise to pull this out into it’s own NSString *sortByProperty so you can easily change it should you decide you want to base your sections/sorting on Author, Publication Year, etc.

At this point you’ll probably want to drop this in too:

    [self.tableView reloadData];

So now that we’ve constructed our self.sections, we can finally implement the UITableViewDataSource protocol methods I mentioned above:

#pragma mark -
#pragma mark Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.sections allKeys] count];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return [[[self.sections allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:section];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[self.sections valueForKey:[[[self.sections allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:section]] count];
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return [[self.sections allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
}

This is where things get a little confusing. Since these methods are called with indexPath.section, which an NSInteger, and not the actual section key, we’re forced to get a little tricky. In order to make sure that our section indexes and section titles match up correctly AND are ordered alphabetically, we have to sort allKeys each time and then reference the resulting NSArray. It’s kind of ugly, but I’ve done all the annoying bracket matching for you already.

OK. So now that we have sections out of the way, let’s show some cells…

- (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] autorelease];
    }

    NSDictionary *book = [[self.sections valueForKey:[[[self.sections allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section]] objectAtIndex:indexPath.row];

    cell.textLabel.text = [book objectForKey:@"title"];
    cell.detailTextLabel.text = [book objectForKey:@"description"];

    return cell;
}

That’s it! You should have a great looking sectioned tableView. If you have any questions or comments, you can get a hold me@matt_tuzzolo on Twitter.

Leave a Reply

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