Well this is going to be a fairly short article, but I just wanted to share with you a snippet of magic I discovered quite some time ago. It is something that I posted on StackOverflow and to date is the most up-votes I have received on there, with votes coming in pretty much every day.

The magic is in how you can animate the height change for a UITableViewCell in a normal UITableView.

The source code for this article can be downloaded here.

 

Simplicity Defined

This really is as simple as it gets. First you need to implement all of the standard methods that you are required to for a UITableView datasource and delegate. But the key one to look at is the following…

[objc] – (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// If our cell is selected, return double height
if([self cellIsSelected:indexPath]) {
return kCellHeight * 2.0;
}

// Cell isn’t selected so return single height
return kCellHeight;
}
[/objc]

 

What this does is check whether the cell is currently selected and if so returns double the normal height. kCellHeight is a compiler define and cellIsSelected: is a method which takes an indexPath and returns whether the cell is selected or not. This doesn’t come from the cell itself, instead I am just storing the selected state in an NSMutableDictionary keyed on the indexPath.

Animate The Change

So we know that returning a variable height in the heightForRowAtIndexPath method will result in the table sizing the cells accordingly, but how do we animate the change… well simplicity itself… you simply use these two lines…

[objc] [myTableView beginUpdates];
[myTableView endUpdates];
[/objc]

 

What this results in is the UITableView re-evaluating it’s visible cells and setting their size accordingly but without reloading the data, and the best part? IT ANIMATES IT!

In the demo project I toggle the selected status of a cell and call these magic two lines in the following method…

[objc light=”true”] – (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
[/objc]

 

The Full Code

The full code for the selection method is as follows…

[objc] – (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Deselect cell
[tableView deselectRowAtIndexPath:indexPath animated:TRUE];

// Toggle ‘selected’ state
BOOL isSelected = ![self cellIsSelected:indexPath];

// Store cell ‘selected’ state keyed on indexPath
NSNumber *selectedIndex = [NSNumber numberWithBool:isSelected];
[selectedIndexes setObject:selectedIndex forKey:indexPath];

// This is where magic happens…
[demoTableView beginUpdates];
[demoTableView endUpdates];
}
[/objc]

 

So download the demo project and see it in action…. marvellous!

Recommended Posts
Showing 44 comments
  • Graham
    Reply

    Amazing publish, I await updates of your stuff.

    • Simon
      Reply

      Thanks Graham, we have a lot more technical tutorials coming so stay tuned!

  • Marc
    Reply

    Hey Simon,

    Love the article, and it’s already helped me tremendously. However, I have a (hopefully quick) few questions:

    I use a custom cell (UITableViewCell class, obviously) to show/hide UILabels upon selection of each cell in my UITableView within a UIView class (think of it as a detail view; when the cell expands, more information is visible, and when it is contracted, the information is hidden again.)

    As you can imagine, this causes problems when used with this bit of code here because the cells automatically deselect once fully expanded, and the labels never have the chance to appear.

    If I remove

    [demoTableView deselectRowAtIndexPath:indexPath animated:TRUE];

    from didSelectRowAtIndexPath I can get the hidden label to appear, but it does not disappear of course, as the cell does not get deselected until I select a different cell.

    How might I make it so that the cell automatically deselects once it returns to its regular height after a user clicks it for the second time? Also, is there a way to limit the table to only one expanded cell at a time?

    Please let me know as my brain is near-hemorrhage trying to fiddle with this,

    Marc

  • Simon
    Reply

    Marc,

    Thanks for the question. If I am right in assuming you are showing / hiding the labels on the cell select then why not keep the cells visible but in the ‘hidden’ portion of the cell (when collapsed). With clipsToBounds turned on, these labels will not be visible as they will be clipped by the smaller cell.

    Does this make sense? If I have misunderstood then feel free to drop me some more of your code to my email [email protected] and I can take a look for you.

    All the best.

  • Marc
    Reply

    Simon,

    You. Are. A. Genius.

    That was probably the simplest solution possible, and just goes to show that looking at a problem from five different angles is no good when the best method is staring you straight in the face.

    I was writing gesture recognition methods, experimenting with replacing the original cell upon selection, and all it takes is one line of code. Fantastic stuff.

    Thanks so much!
    Marc

    [Note, for anyone who stumbles upon this looking for help, what Simon is referring to is:

    self.contentView.clipsToBounds = YES;

    when laying out your cell]

  • Marc
    Reply

    Oh, and also quickly, any thoughts on how to limit the number of expanded cells at one time to just one? Eg expanding one cell returns the previously expanded cell back to its original height.

    I think something like that would dramatically improve the user experience.

    Thanks again for the glaringly obvious solution,

    • Simon
      Reply

      Hi Marc,

      The simplest way is to store a single NSIndexPath as the current selected item instead on an array of indexes.

      Simply set the current index when the selection changes and when the tableview wants a cell height, simply check if the cell index being asked for matches the selected index, if so return the larger height.

      Animating in the normal way will cause the previously expanded cell to contract whilst also enlarging the newly selected one. 🙂

      • raptor
        Reply

        can u explain me this problem in code . i am new to programming

  • Arnold Nefkens
    Reply

    Hi Simon, nice tutorial. Am having the same problem as Marc, but not just with a UILabel, but with a UITextField. regardless if I set clipsToBounds on the self.contentView in the custom cell, or in the setting up the custom cell, or both. the UITexField is always shown over the other cells and is not “hidden” in the cell. Any advice?

    Cheers!

  • Arnold Nefkens
    Reply

    Solved it by hiding the UITextField and other items on Init and setting the hidden bool on the calculate height of the cell.

    • Simon
      Reply

      Hi Arnold, thanks for the comments.

      Glad you got it sorted, not quite sure why clipping was not doing the trick, check the UITextField is a child of the correct view and it is that view you are clipping.

  • KW
    Reply

    Simon,

    Just wanted to confirm that this is something we can use in apps that we submit to Apple for distribution in the App Store, correct? It’s by far the best solution out there for this problem. I imagine you’d want proper credit in any app’s about section as well?

    Cheers!

    • Simon
      Reply

      Hi,

      Glad you found this useful. Absolutely, you can use this in apps you submit to Apple and it will be approved. Credit is nice but not required at all, just let us know the name of the app you use it in so we can take a look! 🙂

      Thanks,

      Simon.

  • sprinrider
    Reply

    Thanks Simon, for the great tutorial.

    I got question about the expand direction, it seems after tap the cell, it will always expand down.
    So how to make the cell to fill up the entire screen(expand up and down, both directions) with animation? can the “beginupdate && endupdate” magic do it?
    thanks!

    • Simon
      Reply

      Hi,

      Glad you found the article useful. I would suggest you set the scroll position of the tableview to the top of the cell in question, and at the same time adjust the height, this *should* cause the cell to be aligned at the top whilst expanding down to fill the whole screen. You need to use – (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated with the scroll position set to UITableViewScrollPositionTop and animated set to false (although if this doesn’t work try setting animated to true).

      I haven’t had chance to try this, but it seems the best approach to me (if it works).

      • sprinrider
        Reply

        Thank you very much for the help! I will give a try!

  • Ibrahim
    Reply

    Awesome Work Thank u 🙂

  • MachOSX
    Reply

    Exactly what I was looking for! Thank you! 🙂

  • Matt
    Reply

    In case it helps others, thought I’d add this comment:

    The simple block described in this post only partially worked for me:
    [myTableView beginUpdates];
    [myTableView endUpdates];

    My table is in a sort of permanent Edit mode so that it can show re-order controls, which means I have to have a sort of custom Edit button that expands the cells and shows the deletion controls next to each one (and hides them again when the user taps the button again, when it’s displaying Done instead of Edit).

    The regular call I was doing to [tableView reloadData] at the end of my custom Edit button’s responder wasn’t animating the change, which is why I went searching and found Simon’s post on this site in the first place. For some reason, while reloadData was properly displaying the table with the enlarged cells and deletion controls, it wasn’t creating any sort of transition to it – very jarring and un-iphone-like!

    After I implemented Simon’s block, I had animation… but no deletion controls! Evidently, the beginUpdates/endUpdates block successfully animates changes to cell heights, but because my action isn’t part of the normal series of steps that requires animations (steps like didSelectRowAtIndexPath or the wholesale reloadData), it wouldn’t call the method triggering the display of deletion controls, ie:

    – (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath

    I’m pretty sure it’s because I’m trying to animate a change to the row height for my entire table in response to a “custom event” (eg tapping a button outside the table itself that also isn’t the standard Edit or + button, instead of a row inside the table), in combination with the fact that my table is already in Edit mode, that the beginUpdates/endUpdates solution isn’t sufficient. My presumption is that a method like didSelectRowAtIndexPath already sets up (or “primes”) the table for animation just for the selection part, although I’m not experienced enough to explain it. And with [tableView reloadData] refusing to animate, I thought I had run out of options. It was in consulting the Apple developer docs that I found something that finally worked – a happy medium, if you will, that married what I needed of reloadData and beginUpates/endUpdates:

    [tableView reloadSections:sectionsForReload withRowAnimation:UITableViewRowAnimationAutomatic];

    Since my table was just one section, I defined sectionsForReload as:
    NSIndexSet *sectionsForReload = [[NSIndexSet alloc] initWithIndex:0];

    This can also be done on a row-by-row basis. I would of course be curious to know what other experiences people have had in this particular situation!

    • Simon
      Reply

      Hi Matt,

      Thanks for the info, it’s interesting that you lose the edit controls with the simple two-line trick, however we’ve only ever used it in ‘display’ mode.

      Thanks for taking the time to post your experience for others to read.

      Simon.

  • Competitor
    Reply

    I hope for the »nerd alert«, that your designer/editor will be fired in the near future.
    thats lame and embarrassing

    • Simon
      Reply

      I disagree, education doesn’t need to be so serious! We hire the best people around, expect more Nerd Alerts, Geek Alerts, Information Overload Alerts etc in the future.

      We appreciate the feedback though, even if you didn’t leave your *real* details 😉

  • Thomas
    Reply

    Super article, thanks very much for that it’s already helped me tremendously. I do have a question however, and it’s probably something that’s quite easy to do, admittedly I’m still very new to Objective C. What I want is for when you click the cell, the text inside it changes as it expands. Now, I thought the best way to do this would be to use the replaceObjectAtIndex method to simply edit my text at the appropriate array position. This is what I tried:

    – (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // If our cell is selected, return double height
    if([self cellIsSelected:indexPath]) {
    return kCellHeight * 2.0;
    [cellContent replaceObjectAtIndex:[self cellIsSelected:indexPath] withObject:@”THIS STUFF HERE”];
    }

    Does that look like the right way to do it? Any feedback is greatly appreciated.

  • bagusflyer
    Reply

    Is there any way to change the animation duration? Thanks

  • Basheer Subei
    Reply

    I’ve been trying forever but this quick fix does not work when a subview of the cell is firstResponder.

    is there a way to basically update the cell’s row height (like you have) while the cell’s subview is first responder (without releasing it from first responder using reloadData)?

    any suggestions? thanks, you’re AWESOME!

    • Basheer Subei
      Reply

      oh, btw, when I said it does not work, I literally meant it does not update the cell’s height (could it be because of the cell’s subview being firstResponder?)

      I tried adding those lines in textViewDidChange but it did not work (using reloadData does it, but it gives so many other problems related to resigning the textView as first responder)…

      • Simon
        Reply

        Hi Basheer,

        You need to have a delegate on your specific table cell which tells the parent table view that the row has been selected, store the selected index as per the code and it will all work. You are simply triggering the selection manually rather than using the table view standard selection methods.

        • Basheer Subei
          Reply

          Hi Simon,

          Thanks for the quick response! But it seems I haven’t explained myself correctly.

          I understand the code you posted in blog (and all the delegate stuff), but I’m trying to do something slightly different.

          I have a textView as a subview of a cell.

          I’m trying to call the beginUpdates and endUpdates inside textViewDidChange so I can resize the cell (my heightForRowAtIndex method is working perfectly, i checked). Whenever I call beginUpdates and endUpdates in textViewDidChange, NOTHING happens! I tried calling [tableView reloadData] instead, and it works (it resizes the cell). However, reloadData resigns my textView as first responder (and that causes a whole slew of other problems that are too hard to handle)…

          do you have any ideas or suggestions? I’m about to give up on live resizing for cells (does any app have this?)

          thanks again!

  • Granfalloner
    Reply

    This approach seems to have one pity drawback. Small quote for Apple’s documentation: “If you do not make the insertion, deletion, and selection calls inside this block, table attributes such as row count might become invalid.” (http://developer.apple.com/library/ios/documentation/uikit/reference/UITableView_Class/Reference/Reference.html#//apple_ref/occ/instm/UITableView/beginUpdates)

    • Simon
      Reply

      I think you have misread what Apple are saying. They are not saying that by not including insert or delete calls when using BeginUpdates that the row count will become invalid, they are saying that performing inserts and deletes without using the Begin/EndUpdates may result in the row count being invalid. There is no issue with using begin / end updates without actually changing the underlying dataset.

      • Granfalloner
        Reply

        Sorry, but your interpretation is not correct. Here is more detailed explanation: http://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/ManageInsertDeleteRow/ManageInsertDeleteRow.html#//apple_ref/doc/uid/TP40007451-CH10-SW9
        And that means, that your approach to animating cell height changes is dangerous.

        • Simon
          Reply

          Thanks for the link but I can confirm our interpretation is correct and you are misreading what Apple are saying.

          To quote Apple… “At the conclusion of a block—that is, after endUpdates returns—the table view queries its data source and delegate as usual for row and section data. Thus the collection objects backing the table view should be updated to reflect the new or removed rows or sections.”

          After EndUpdates is called row sections are re-queried and WILL be correct. HOWEVER if you do inserts and updates OUTSIDE of begin / end updates then the datasource IS NOT queried and so row counts can remain incorrect.

          • Granfalloner

            Probably your are right. There is still some uncertainty, as it seems to me, because of a note in the last referenced article: “you CAN call these insertion and deletion methods outside of an animation block”, and this doesn’t fit well in your suggested model, but on the other hand, your clarifications also make sense. And the last decisive argument – I tested it, and it works 🙂
            Thanks for answers!

        • Simon
          Reply

          If you have any concerns then I would suggest you don’t use it, alternatively you can get this approach confirmed by Apple as being safe for use (it is in use by a large number of applications).

  • Vinicius
    Reply

    Really nice job! Saved my project!
    I’m using it to display another 2 UILabels when the user selects it, my problem is, i set the UILabel to Hidden when the user deselects it on didSelectRowAtIndexPath, every time the table view is loaded in the method cellForRowAtIndexPath the texts hidden again
    BOOL flag = ![self cellIsSelected:indexPath];
    if(flag)
    {
    [cell.texto2 setHidden:YES];
    [cell.texto3 setHidden:YES];
    }

    any correct way to set the text to selected in there?

    And thanks again!

  • Sergey
    Reply
  • Dmitry
    Reply

    Thanks a lot!
    Your very helpful post saved a lot of my time 🙂

  • Federico Cappelli
    Reply

    Great article! thank you 🙂

pingbacks / trackbacks
  • iPhone – Expand UITableView with custom cell and hide/show labels on select | taking a bite into Apple

    […] need to expand/contract cells upon selection, which I have done using the awesome tutorial here. However, I also need to show/hide UILabels upon selection–like a detail view, as you expand […]

  • iPhone – Expand UITableView with custom cell and hide/show labels on select - Programmers Goodies

    […] need to expand/contract cells upon selection, which I have done using the awesome tutorial here. However, I also need to show/hide UILabels upon selection–like a detail view, as you expand […]

  • Enabling & Disabling UI Toolbar in Expanded Cell | PHP Developer Resource

    […] some my code. I’m using Simon Lee’s tutorial to expand the […]

  • Animate UITableView Cell Height Change

    […] via Locassa tech blog […]

  • Komodo | UITableViewController 表格动画

    […] 外国人写的Animate UITableView Cell Height Change,有源代码。 […]

  • UITableView Magic | Thought Repository

    […] just discovered this post, which taught me a bit of simple magic that I thought was worth […]

Leave a Comment