Sapan Diwakar

Software developer

Follow me on Twitter Check out my code on GitHub View some of my designs on Dribbble Take a look at my Linked In profile

Nested Sections in UITableView

Sometimes, there might be cases where you would like to display nested sections in a UITableView. For example, consider an app organizing groups taxonomically like so

Sports  
  Bat-and-ball
    Baseball
    Softball
    Cricket
  Hockey
    Field Hockey
    Ice Hockey
    Roller Hockey
Engineering  
  Computer Science
    Software Engineer
    Electrical Engineer

A UITableView really isn't designed to show more than two levels of a hierarchy, as sections and rows. If you want to show more than two levels, a "drill-down" approach used in most iOS apps, where tapping a row presents another UITableView on the navigation stack. But is this the only (or the best) option? Not really. For one project that I recently worked on, doing two levels of heirarchy in the same table view was the best way, so I set my eyes on building it manually.

As it turned out, it wasn't too much work. The trick was to have two kinds of rows in the table view. One to represent the second level of sections and another to represent the normal rows in the tableview. Let's say you have a two level array (say sections) to represent the items in your table view. For example, here is the representation that you have for the above table

[ // All sections
    [ // Sports section
        [Baseball, Softball, Cricket], // Bat-and-Ball sub-section
        [Field Hockey, Ice Hockey, Roller Hockey] // Hockey subsection
    ], [ // Engineering section
        [Software Engineer, Electrical Engineer] // Computer Science subsection
    ]
]

The total number of sections that we have are just the number of top level sections. The number of rows in each top level section would be the number of subsections + the number of rows in each subsection.

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

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSArray *sectionItems = self.sections[(NSUInteger) section];
    NSUInteger numberOfRows = sectionItems.count; // For second level section headers
    for (NSArray *rowItems  in sectionItems) {
        numberOfRows += rowItems.count; // For actual table rows
    }
    return numberOfRows;
}

Now, all we need to think about is how to create the rows for the table view. Set up two prototypes in the storyboard with different reuse identifiers, one for the section header and another for row item and just instantiate the correct one based on the asked index in the data source method.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSMutableArray *sectionItems = self.sections[(NSUInteger) indexPath.section];
    NSMutableArray *sectionHeaders = self.sectionHeaders[(NSUInteger) indexPath.section];
    NSIndexPath *itemAndSubsectionIndex = [self computeItemAndSubsectionIndexForIndexPath:indexPath];
    NSUInteger subsectionIndex = (NSUInteger) itemAndSubsectionIndex.section;
    NSInteger itemIndex = itemAndSubsectionIndex.row;

    if (itemIndex < 0) {
        // Section header
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SECTION_HEADER_CELL" forIndexPath:indexPath];
        cell.textLabel.text = sectionHeaders[subsectionIndex];
        return cell;
    } else {
        // Row Item
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ROW_CONTENT_CELL" forIndexPath:indexPath];
        cell.textLabel.text = sectionItems[subsectionIndex][itemIndex];
        return cell;
    }
}

- (NSIndexPath *)computeItemAndSubsectionIndexForIndexPath:(NSIndexPath *)indexPath {
    NSMutableArray *sectionItems = self.sections[(NSUInteger) indexPath.section];
    NSInteger itemIndex = indexPath.row;
    NSUInteger subsectionIndex = 0;
    for (NSUInteger i = 0; i < sectionItems.count; ++i) {
        // First row for each section item is header
        --itemIndex;
        // Check if the item index is within this subsection's items
        NSArray *subsectionItems = sectionItems[i];
        if (itemIndex < (NSInteger) subsectionItems.count) {
            subsectionIndex = i;
            break;
        } else {
            itemIndex -= subsectionItems.count;
        }
    }
    return [NSIndexPath indexPathForRow:itemIndex inSection:subsectionIndex];
}

It takes a bit of work, but when it comes out, it looks amazing. So much better than presenting two table views with drill down. Bonus points for overriding scrollViewDidScroll: to mimic the floating behaviour for the subsection headers. I think its getting a bit too much to take up for one post, so I leave this for you to figure out.

Here's how it looks:

If you need any help in setting this up, feel free to shoot me an email.