Displaying a details view to show data after the user selects something from a table on the iPhone is pretty much the standard and it sure is useful in many cases. Fact is that it’s not always the fastest or correct way of showing details, specially if the amount of details left to show is low. I’ve thought in the past about making a UITableViewCell expand and collapse, something like OS X has, but while my (quick and dirty) efforts failed on iPhone OS up to 2.2.1 I recently came across the missing piece to make it work on iPhone OS 3.0+.

As you’ll probably know, there are only 2 ways of setting a cell’s height inside a UITableView. You can set the property for the default row height on the UITableView object or use the delegate. The delegate is by far the most flexible of both solutions and isn’t hard at all to use:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 64; }

This delegate is rarely invoked however. Without deep research I think it would only be called when refreshing the list of cells, data or changing the size/framing of the UITableView. Those situations could be exploited to archive the final goal but depending on the number and type of data it could bring serious performance problems. As far as iPhone OS 2.2.1 goes, I tried to find a way but in the end I gave up on it after finding a 3.0+ solution that worked better than I expected. I say better because I was expecting to have to implement basic animation but out-the-box it gives us a fine default. The implementation is fairly simple for a simple scenario, I’ll demonstrate how to implement the simplest which is to expand the currently selected cell while collapsing all the other cells.

// Somewhere in your header: NSIndexPath *selectedCellIndexPath; // And in the implementation file: - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { selectedCellIndexPath = indexPath; // Forces the table view to call heightForRowAtIndexPath [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { // Note: Some operations like calling [tableView cellForRowAtIndexPath:indexPath] // will call heightForRow and thus create a stack overflow if(selectedCellIndexPath != nil && [selectedCellIndexPath compare:indexPath] == NSOrderedSame) return 128; return 64; }

It’s that simple but understanding how it works will help you shape it better to your needs. Basically the reloadRowsAtIndexPaths call makes sure the UITableView will reload those cells and invoke heightForRowAtIndexPath in the process. The reloadRowsAtIndexPaths is the magic that the iPhone OS’s before 3.0 don’t have. Another noteworthy subject is why I store the NSIndexPath the way I do, and the reason for that is simply because inside the heightForRowAtIndexPath we can’t call certain methods like cellForRowAtIndexPath without causing a stack overflow.

I’m fully aware that this code isn’t on its most reusable form but depending on the application and objective there are just too many different ways of encapsulating it. Perhaps the most useful and popular will be to inherit the UITableViewCell and implement a Height property on it. Adjusting the didSelectRowAtIndexPath override (or other points in the controller) to simply set the Height property and finally adjusting the heightForRowAtIndexPath code to use that property instead of the variable. That solution would enable for multiple expanded cells at the same time while my original snippet only allows for one. How you do it depends on your needs alone.

Tags: Cocoa, expand/collapse, iPhone, Objective C, Tricks, uitableview, uitableviewcell