Blog

Expand and collapse UITableViewCells

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.

Post to Twitter Post to Delicious Post to Digg Post to Facebook Post to Reddit Post to StumbleUpon

Tags: , , , , , ,

10 Responses to “Expand and collapse UITableViewCells”

  1. daniel says:

    may be use the “[tableView indexPathForSelectedRow]” to check the selected row

    – (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
    }

    – (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    if( [tableView indexPathForSelectedRow] && [[tableView indexPathForSelectedRow] isEqual:indexPath] ) {
    return 128;
    }

    return 64;
    }

  2. alexmipego says:

    iirc that also fired the heightForRowAtIndexPath causing a stack overflow, need to test again.

  3. Ricki says:

    Thanks Alexandre!
    I was looking for the “right piece of information” and this
    was certainly it.

  4. Mark says:

    Thanks Alexandre! This is exactly what I was after, thanks a lot

  5. treasure says:

    thanks. that is great…
    how do you return the cell to previous (default) height when user touches the cell again?

  6. Sanjay says:

    Very help tuts for me.

    Unfortunately i am facing another problem with this kind of animation.

    ====Reusable Cell=== problem.

    Cells are overriding with height of 128 more than one.

    Help me out plz

  7. bob says:

    [selectedCellIndexPath compare:indexPath] == NSOrderedSame
    should be written as
    [selectedCellIndexPath isEqual:indexPath]

  8. April says:

    Hi,

    can we download the code for this?

    Thank you so much for sharing this :)

  9. Hao Zhe XU says:

    Hi, if I change the height of the table’s footer view, how do I reload it?

    Thanks!

  10. alexmipego says:

    Hi, if I recall correctly, the headerView and footerView of a UITableView need to be reset. I usually just do all the adjustments I need to it and then do a “table.headerView = table.headerView;”. Replace headerView with footerView and it should do the same.

Leave a Reply

For spam filtering purposes, please copy the number 4708 to the field below: