TigerCoding
TigerCoding

Reputation: 8720

Why do my table view cells disappear when reloaded using reloadRowsAtIndexPaths?

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.

Upvotes: 21

Views: 18894

Answers (12)

apalvai
apalvai

Reputation: 597

@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.

Upvotes: 0

Vrezh Gulyan
Vrezh Gulyan

Reputation: 228

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.

Upvotes: 0

user349819
user349819

Reputation:

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.

Upvotes: 21

Collin
Collin

Reputation: 6760

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?

Upvotes: 9

Matthias
Matthias

Reputation: 1026

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
     ...
     */

}

Upvotes: 1

Dan Mardale
Dan Mardale

Reputation: 51

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."

Upvotes: 0

vishy
vishy

Reputation: 3231

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];
}

Upvotes: 0

Deniz Mert Edincik
Deniz Mert Edincik

Reputation: 4363

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];

Upvotes: 10

Dhara
Dhara

Reputation: 4089

Try this hope it will help u Cell Expansion

Upvotes: -1

Nathan Kinsinger
Nathan Kinsinger

Reputation: 25041

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.

Upvotes: 5

Danra
Danra

Reputation: 9926

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.

Upvotes: 2

Srikar Appalaraju
Srikar Appalaraju

Reputation: 73688

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...

Upvotes: 1

Related Questions