21

I have a bare-bones sample project here:

http://dl.dropbox.com/u/7834263/ExpandingCells.zip

In this project, a UITableView has a custom UITableViewCell. In each cell are 3 UIViews containing a label.

The goal is to expand the cell when tapped, then collapse it when tapped again. The cell itself must change it’s height to expose the subviews. Inserting or removing rows is unacceptable.

The demo project works almost as expected. In fact, in iOS 4.3 it works perfect. Under iOS 5, however, when the rows collapse, the previous cells magically disappear.

To re-create the problem, run the project in the simulator or device with iOS 5 and tap the first cell to expand it. Then tap the cell again to collapse it. Finally, tap the cell directly underneath it. The previous one disappears.

Continuing the tapping for each cell in the section will cause all the cells to disappear, to where the entire section is missing.

I’ve also tried using reloadData instead of the current setup, but that ruins the animations and feels a bit like a hack anyway. reloadRowsAtIndexPaths should work, but the question is why doesn’t it?

See images of what's happening below:

Table appears:

table appears

Cell expands:

table expands

Cell collapses:

table collapses

Cell disappears (when tapping the cell underneath):

cell disappears

Keep repeating until the entire section disappears:

section disappears

EDIT: Overriding the alpha is a hack, but works. Here is another 'hack' that fixes it as well but WHY does it fix it?

JVViewController.m line 125:

if( previousIndexPath_ != nil )
{
    if( [previousIndexPath_ compare:indexPath] == NSOrderedSame ) currentCellSameAsPreviousCell = YES;

    JVCell *previousCell = (JVCell*)[self cellForIndexPath:previousIndexPath_];

    BOOL expanded = [previousCell expanded];
    if( expanded )
    {
        [previousCell setExpanded:NO];
        [indicesToReload addObject:[previousIndexPath_ copy]];
    }
    else if( currentCellSameAsPreviousCell )
    {
        [previousCell setExpanded:YES];
        [indicesToReload addObject:[previousIndexPath_ copy]];
    }

    //[indicesToReload addObject:[previousIndexPath_ copy]];
}

EDIT 2:

Made a few minor changes to demo project, worth checking out and reviewing JVViewController didSelectRowAtIndexPath method.

TigerCoding
  • 8,710
  • 10
  • 47
  • 72
  • Overriding setAlpha: in JVCell to block the setAlpha:0 seems to be a workaround, and I don't see any negative side-effects, but it's ugly. I'll keep poking at it, see if I can figure out why it wants to hide the cells. – davehayden Mar 11 '12 at 01:42
  • i have an example similar to this u want it? – Dhara Mar 20 '12 at 11:03
  • If it does the same thing, sure. It needs to have images for backgrounds, and change the cell height when it expands. It cannot add or remove cells. It must also be animated. You can post it in an answer and add the source code or link to it. What is it that your code does differently? – TigerCoding Mar 20 '12 at 11:19

12 Answers12

21

Your problem is in setExpanded: in JVCell.m, you are directly editing the frame of the target cell in that method.

- (void)setExpanded:(BOOL)expanded
{
    expanded_ = expanded;

    CGFloat newHeight = heightCollapsed_;
    if( expanded_ ) newHeight = heightExpanded_;

    CGRect frame = self.frame;
    frame.size.height = newHeight;
    self.frame = frame;
}

Update it to:

- (void)setExpanded:(BOOL)expanded
{
    expanded_ = expanded;
}

Then remove the call to -reloadRowsAtIndexPaths:withRowAnimation: at line 163 of JVViewController.m and it will animate as expected.

-reloadRowsAtIndexPaths:withRowAnimation: expects different cells to be returned for the provided indexPaths. Since you are only adjusting sizes -beginUpdates & -endUpdates is sufficient to layout the table view cells again.

pkamb
  • 33,281
  • 23
  • 160
  • 191
  • 2
    As a sidenote, the reason the alpha was being set to zero was because the table was treating one cell as both an old and a new cell. The behavior of the table view is to fade the old cell out with the new cell behind it. So its alpha was being set to 1 as a "new" cell, but then faded from 1 to zero as an "old" cell. –  Mar 21 '12 at 20:02
  • +1 - verified that this works in the simulator for both 4.3 and 5.1. – Scott Marks Mar 21 '12 at 20:32
  • This makes the most sense of all the answers. I'd rather not force the cell to reload if it can simply change it's height automatically. The animations are smooth as well. Very nice! But is it a 'hack' to use beginUpdates and endUpdates? Is my code using those methods properly? – TigerCoding Mar 22 '12 at 07:04
  • This is not a hack, and is the appropriate way to do this. The reference does not explicitly state it, but one of the functions of the begin/endUpdates is that it will animate to a new layout. beginUpdates/endUpdates is the only method that will call for information from the delegate (where your rowHeight is being returned) and animate based on the changes, without reloading the cells. The only viable alternative is to call reloadData, and do all the animating yourself from one state to the next, which I would personally avoid. –  Mar 22 '12 at 14:23
  • Looks like your answer is most appropriate, even though @Deniz also provided a way to make it work. Thanks for your help in this I almost thought it wouldn't be resolved. Now my table view's are looking very nice. Thanks again! – TigerCoding Mar 22 '12 at 14:52
  • No problem, and thanks for providing the source. Without that I would have had a hard time pinning the problem down. –  Mar 22 '12 at 15:57
10

May be I am missing a point, but why dont you just use:

UITableViewRowAnimationNone

I mean instead of :

[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];

use

[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationNone];
Deniz Mert Edincik
  • 4,336
  • 22
  • 24
  • To think all this trouble over that. This is my second bounty for this question, and I was beginning to think it wouldn't be solved. the questions that remains are WHY does this work, and should I be using begin/end update methods? Thanks Deniz – TigerCoding Mar 21 '12 at 13:20
  • This one fixed the problem in my case. – Kof Apr 17 '13 at 18:49
9

To animate the height changes of a tableView just call.

[tableView beginUpdates];
[tableView endUpdates];

Don't call reloadRowsAtIndexPaths:

See Can you animate a height change on a UITableViewCell when selected?

Community
  • 1
  • 1
Collin
  • 6,720
  • 4
  • 26
  • 29
  • I'm glad I scrolled to the bottom of the page to see that answer :) – lukasz Oct 29 '14 at 23:00
  • Can you explain why beginupdates should be the case instead? Also, this doesn't explain why reloadRow will make the cell disappear @.@ – Happiehappie Nov 09 '16 at 06:43
  • thanks this worked for me, should be the accepted answer! – Vrutin Rathod Mar 09 '18 at 09:43
  • @Happiehappie https://developer.apple.com/documentation/uikit/uitableview/1614935-reloadrowsatindexpaths says `Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out. Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.` – CyberMew Jan 03 '22 at 03:12
  • @Happiehappie sorry should've lead with https://developer.apple.com/documentation/uikit/uitableview/1614908-beginupdates, which says `You can also use this method followed by the endUpdates() method to animate the change in the row heights without reloading the cell.` – CyberMew Jan 03 '22 at 03:35
5

The cell that is fading out is the previous cell that is not changing size. As the documentation of reloadRowsAtIndexPaths:withRowAnimation: states:

The table animates that new cell in as it animates the old row out.

What happens is the opacity is set to 1 then immediately set to 0 and so it fades out.

If both the previous and the new cell change size then it works as intended. This is because the begin/end Updates notice the height changes and create new animations on those cells overriding the reloadRowsAtIndexPaths:withRowAnimation: ones.

Your problem is due to abusing reloadRowsAtIndexPaths:withRowAnimation: to resize the cells when it's intended for loading new cells.

But you don't need reloadRowsAtIndexPaths:withRowAnimation: at all. Just change the expanded state of the cells and do the begin/end updates. That will handle all the animation for you.

As a side note I found the blue selection a little annoying, in JVCell set the selectedBackgroundView to the same image as the backgroundView (or create a new image that has the correct look of a selected cell).


EDIT:

Move the statement adding previousIndexPath_ to indicesToReload to the if statement (at line 132) so that it is only added if the previous cell was expanded and needs to resize.

if( expanded ) {
    [previousCell setExpanded:NO];
    [indicesToReload addObject:[previousIndexPath_ copy]];
}

This removes the case where the previous collapsed cell would disappear.

Another option would be to set previousIndexPath_ to nil when the current cell is collapsed and only set it when a cell expands.

This still feels like a hack. Doing both the reloadRows and the begin/end Updates causes the tableView to reload everything twice but both seem to be needed to animate correctly. I suppose if the table is not too large this won't be a performance problem.

Nathan Kinsinger
  • 23,641
  • 2
  • 30
  • 19
  • I tried commenting out line #149 in JVViewController (reloadRowsForIndexPaths) but the height changes still don't smoothly animate. Notice how when clicking on the second cell the previous one kind of 'snaps' back in place? You can see the table view background in between the cells for a brief moment. How can this be avoided? Regarding the blue color, I agree and added a few lines of code in JVCell setupImageView method to also set the selected background image as the same image. This is only a demo project, the full one would use different graphics. Download the updated project. – TigerCoding Mar 19 '12 at 04:41
  • I see what you are saying about the center animation. Very bizarre that the animation time is different depending on the location in the table. I created more cells in each section and all the center cells behave the same. Only the top and bottom cell animate correctly. – Nathan Kinsinger Mar 19 '12 at 04:59
2

Short, pragmatic answer: Changing UITableViewRowAnimationAutomatic to UITableViewRowAnimationTop solves the issue. No more disappearing rows! (tested on iOS 5.1)

Another short, pragmatic answer, since UITableViewRowAnimationTop is said to cause its own issues: Create a new cell view instead of modifying the existing one's frame. In a real app the data displayed in the cell view is supposed to be in the Model part of the app anyway, so if properly designed it shouldn't be a problem to create another cell view which displays the same data only in a different manner (frame in our case).

Some more thoughts regarding animating the reload of the same cell:

UITableViewRowAnimationAutomatic seems to resolve to UITableViewRowAnimationFade in some cases, which is when you see the cells fading away and disappearing. The new cell is supposed to fade in while the old one fades out. But here the old cell and the new one are one and the same - So, could this even work? In the Core Animation level, Is it possible to fade out a view AND fade it in at the same time? Sounds dubious. So the result is that you just see the fade out. This could be considered an Apple bug, since an expected behavior could be that if the same view has changed, the alpha property wouldn't be animated (since it can't animate both to 0 and to 1 at the same time), but instead just the frame, color etc. would be animated.

Note the problem is just in the animation's display - if you scroll away and back, everything will appear correctly.

In iOS 4.3 the Automatic mode might have been resolved to something other than Fade which is why things work there (as you write they do) - I didn't dig into that.

I don't know why iOS chooses the Fade mode when it does. But one of the cases it does is when your code asks reloads a previously tapped cell, which is collapsed, and is different than the current tapped cell. Note the previously tapped cell is always reloaded, this line in your code is always called:

[indicesToReload addObject:[previousIndexPath_ copy]];

This explains the magic disappearing cells scenario you have described.

By the way, the beginUpdates/endUpdates seem like a hack to me. This pair of calls is just supposed to contain animations, and there aren't any animations you are adding in addition to the rows you already asked to reload. All it did in this case is magically cause the Automatic mode to not choose Fade in some cases - But this just obscured the problem.

A final note: I played around with the Top mode and found it can also cause problems. For example plugging the following code makes cells disappear funkily:

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

Not sure if there is a real issue here (similar to the one with fading a view in and out at the same time), or maybe an Apple bug.

Danra
  • 9,546
  • 5
  • 59
  • 117
  • Thanks for the attempt, but I also tried "Top" before with the same results. It worked at first but then playing with it I found it has it's own issues. I also agree the begin/end updates are a hack, but I'm able to get it working. I would simply like to know why it isn't working or if it is indeed a bug. I also filed a bug report with Apple, but I don't know if it is indeed a bug. Still waiting to hear back from them. – TigerCoding Mar 17 '12 at 14:01
  • @Javy Updated answer with another option for solution - using a different cell when changing the frame. By the way - you wrote you are not interested in calling -reloadData, but your -heightForRowAtIndexPath: method is called each time the tableView is redisplayed for all cells, and in your implementation this calls -cellForRowAtIndexPath:. So all cells are re-retrieved, which I think is the bulk of what reloadData does anyway. – Danra Mar 17 '12 at 14:24
  • If you try reloadData it kills the animations. Give it a try. The cells kind of 'snap' into place instead of animating. "using a different cell when changing the frame." Could you clarify more? I didn't see a change in your answer text. – TigerCoding Mar 17 '12 at 14:39
  • "Another short, pragmatic answer, since UITableViewRowAnimationTop is said to cause its own issues: Create a new cell view instead of modifying the existing one's frame. In a real app the data displayed in the cell view is supposed to be in the Model part of the app anyway, so if properly designed it shouldn't be a problem to create another cell view which displays the same data only in a different manner (frame in our case)." – Danra Mar 17 '12 at 14:43
  • Hmmm, that could work, but the app shouldn't have to do that. It's still kind of a hack, like setting alpha or using being/end updates. There may not be a solution to this problem if it is in fact a bug in the SDK. – TigerCoding Mar 17 '12 at 14:51
  • It's not a hack at all. The begin/end updates works for whatever unknown reason and can break in a different iOS version (and in fact does break if you just repeat the call to -reloadRowsAtIndexPaths: one more time, try it out), the alpha is actually going against the fade animation that's going on and is also likely to break. Fading from a cell view to a different cell view could very well be what Apple engineers had in mind when they implemented the feature - It's reusing the same cell that's unconventional. Apple's, and other code almost always returns a new cell from -cellForRowAtIndexPath – Danra Mar 17 '12 at 15:08
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/8994/discussion-between-danra-and-javy) – Danra Mar 17 '12 at 15:09
  • What about when someone makes a reference to a given cell, then that cell gets reloaded? Doesn't that cause breakage? I've seen quite a few code examples that have a view controller hold a reference to a given cell for the purpose of animation, etc. Or does the reference stay the same? – TigerCoding Mar 17 '12 at 16:18
  • A UITableViewCell is just a UIView, if you create a new one it will be a different cell. – Danra Mar 17 '12 at 17:50
  • I agree the idea could work, but I don' think it's the way it's supposed to be written. Are users supposed to reload cells every time they change the height of a cell? It just doesn't seem right. – TigerCoding Mar 18 '12 at 03:01
1

When you use this method, you have to be sure that you are on the main thread. Refreshing a UITableViewCell as follow should do the trick :

- (void) refreshTableViewCell:(NSNumber *)row
{
    if (![[NSThread currentThread] isMainThread])
    {
        [self performSelector:_cmd onThread:[NSThread mainThread]  withObject:row waitUntilDone:NO];
        return;
    }

    /*Refresh your cell here
     ...
     */

}
Matthias
  • 999
  • 11
  • 25
1

I just downloaded your project & found this section of code in didSelectRowAtIndexPath delegate where reloadRowsAtIndexPaths is used.

[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView beginUpdates];
[tableView endUpdates];

instead of the above why don't you try this?

[tableView beginUpdates];
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];

The reason i am suggesting this is that I believe reloadRowsAtIndexPaths:... only works when wrapped inbetween calls to:

- (void)beginUpdates;
- (void)endUpdates;

Outside of that, behavior is undefined and as you've discovered, fairly unreliable. Quoting relevant part of "Table View Programming Guide for iPhone OS":

To animate a batch insertion and deletion of rows and sections, call the insertion and deletion methods within an animation block defined by successive calls to beginUpdates and endUpdates. If you don’t call the insertion and deletion methods within this block, row and section indexes may be invalid. beginUpdates...endUpdates blocks are not nestable.

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.

The reloadSections:withRowAnimation: and reloadRowsAtIndexPaths:withRowAnimation: methods, which were introduced in iPhone OS 3.0, are related to the methods discussed above. They allow you to request the table view to reload the data for specific sections and rows instead of loading the entire visible table view by calling reloadData.

There could be other valid reason but let me mull on this a bit, since i have your code too I could muck around with it. Hopefully we should figure it out...

Community
  • 1
  • 1
Srikar Appalaraju
  • 71,928
  • 54
  • 216
  • 264
  • If you place beginUpdates before reloadRowsAtIndexPaths the cells immediately disappear when tapped. Placing it after reloadRowsAtIndexPaths works. Try out the test project and see what happens. Regarding the Apple docs, "To animate a batch insertion and deletion of rows and sections..." I'm not inserting or deleting rows or sections, I'm simply changing the height of cells, nothing more. I'm using reload so it knows to check their height since it changed. ;) – TigerCoding Mar 16 '12 at 03:47
  • cool, let me keep this answer as is. Will try some stuff & get back to you on this... – Srikar Appalaraju Mar 16 '12 at 03:50
0

This problem is caused by returning cached cells in cellForRowAtIndexPath. The reloadRowsAtIndexPaths is expecting to get fresh new cells from cellForRowAtIndexPath. If you do that you will be ok ... no workarounds required.

From Apple doc: "Reloading a row causes the table view to ask its data source for a new cell for that row."

Dan Mardale
  • 51
  • 1
  • 2
0

I had a similar issue where I wanted to expand a cell when a switch is activated to display and extra label and button in the cell that is normally hidden when the cell is at its default height (44). I tried various versions of reloadRowsAtPath to no avail. Finally I decided to keep it simpler by adding a condition at heightForRowAtIndexPath like so:

    override func tableView(tableView: UITableView,heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    if ( indexPath.row == 2){
        resetIndexPath.append(indexPath)
        if resetPassword.on {
            // whatever height you want to set the row to
            return 125
        }
    }
    return 44
}

And in whatever code you want to trigger the expanding of the cell just insert tableview.reloadData(). In my case it was when a switch was turned on to indicate the desire to reset a password.

        @IBAction func resetPasswordSwitch(sender: AnyObject) {

        tableView.reloadData()
        }

There is no lag with this approach, no visible way to see that the table reloaded and the expansion of the sell is done gradually like you'd expect. Hope this helps someone.

Vrezh Gulyan
  • 218
  • 3
  • 6
0

@Javy, i noticed some weird behavior while testing your app.

While running on iPhone 5.0 simulator the previousIndexpth_ variable is of

class NSArray (it looks like.) here is the debugger output

 (lldb) po previousIndexPath_
    (NSIndexPath *) $5 = 0x06a67bf0 <__NSArrayI 0x6a67bf0>(

    <JVSectionData: 0x6a61230>,

    <JVSectionData: 0x6a64920>,

    <JVSectionData: 0x6a66260>
    )

    (lldb) po [previousIndexPath_ class]

    (id) $7 = 0x0145cb64 __NSArrayI

Whereas in iPhone 4.3 simulator it is of type NSIndexPath.

lldb) po [previousIndexPath_ class]

(id) $5 = 0x009605c8 NSIndexPath

(lldb) po previousIndexPath_

(NSIndexPath *) $6 = 0x04b5a570 <NSIndexPath 0x4b5a570> 2 indexes [0, 0]

Are you aware of this issue? Not sure whether this will help but thought of letting you know.

Esha
  • 1,328
  • 13
  • 34
apalvai
  • 587
  • 5
  • 11
0

I think you should not retain the previous indexPath when the cell is not expanded, try by modifying you did select method like the below, its working fine..

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
BOOL currentCellSameAsPreviousCell = NO;
NSMutableArray *indicesToReload = [NSMutableArray array];

if(previousIndexPath_ != nil)
{
    if( [previousIndexPath_ compare:indexPath] == NSOrderedSame ) currentCellSameAsPreviousCell = YES;

    JVCell *previousCell = (JVCell*)[self cellForIndexPath:previousIndexPath_];

    BOOL expanded = [previousCell expanded];
    if(expanded) 
    {
     [previousCell setExpanded:NO];
    }
    else if  (currentCellSameAsPreviousCell)
    {
        [previousCell setExpanded:YES];
    }
    [indicesToReload addObject:[previousIndexPath_ copy]];

    if (expanded)
        previousIndexPath_ = nil;
    else
        previousIndexPath_ = [indexPath copy];         
}

if(currentCellSameAsPreviousCell == NO)
{
    JVCell *currentCell = (JVCell*)[self cellForIndexPath:indexPath];

    BOOL expanded = [currentCell expanded];
    if(expanded) 
    {
        [currentCell setExpanded:NO];
        previousIndexPath_ = nil;
    }

    else
    {
        [currentCell setExpanded:YES];
        previousIndexPath_ = [indexPath copy];
    }

    // moving this line to inside the if statement blocks above instead of outside the loop works, but why?
    [indicesToReload addObject:[indexPath copy]];


}

// commenting out this line makes the animations work, but the table view background is visible between the cells

[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];

// using reloadData completely ruins the animations
[tableView beginUpdates];

 [tableView endUpdates];
}
vishy
  • 3,241
  • 1
  • 20
  • 25
  • If you run this demo with your code change, the animations snap instead of running smoothly and parts of the background are visible between cells. – TigerCoding Mar 22 '12 at 07:02
  • Yes, I just tried your code changes before I posted the last comment. Notice in the simulator the space between some of the cells doesn't animate at the same time and you can see the background between them. It looks pretty bad. @Saltymule has a good answer above that only resizes the cells instead of reloading them that works pretty good. – TigerCoding Mar 22 '12 at 08:17
-1

Try this hope it will help u Cell Expansion

Dhara
  • 4,093
  • 2
  • 36
  • 69
  • This does not apply to the problem. He is inserting and removing rows. The rows to to change their heights dynamically. – TigerCoding Mar 20 '12 at 15:17
  • 1
    @Javy he is not removing its just checking whether the row is open if open then it will return rows count >0 else not. ok np. – Dhara Mar 21 '12 at 05:02
  • In the interest of fairness I had another look at the link above but he doesn't show animated images which is a big part of this problem. I do appreciate your help so please don't take it the wrong way. Thank you Dhara. – TigerCoding Mar 22 '12 at 14:52