当使用reloadRowsAtIndexPaths重新加载表格视图单元时,为什么我的表格视图单元会消失?

21
我这里有一个基础的示例项目:

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

在这个项目中,一个UITableView有一个自定义的UITableViewCell。每个单元格都包含3个包含标签的UIView。
目标是在点击时扩展单元格,然后再次点击时折叠它。单元格本身必须更改其高度以公开子视图。插入或删除行是不可接受的。
演示项目几乎按预期工作。事实上,在iOS 4.3中,它的表现非常好。然而,在iOS 5下,当行折叠时,以前的单元格会神奇地消失。
要重新创建问题,请在模拟器或带有iOS 5的设备中运行该项目,然后点击第一个单元格以展开它。然后再次点击单元格以折叠它。最后,直接在其下方点击单元格。之前的单元格会消失。
继续点击该部分中的每个单元格将导致所有单元格消失,从而整个部分都消失了。
我还尝试使用reloadData而不是当前设置,但那会破坏动画效果,并且感觉有些像黑客。 reloadRowsAtIndexPaths应该起作用,但问题是为什么不起作用?
请参见下面的图像以查看发生的情况:
表格出现:

table appears

单元格扩展:

table expands

单元格折叠:

table collapses

单元格消失(在点击其下方的单元格时):

cell disappears

重复执行,直到整个部分消失:

section disappears

编辑: 覆盖alpha是一种hack方法,但有效。这里有另一种“hack”方法,也可以解决它,但为什么它会解决呢?

JVViewController.m第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]];
}

编辑 2:

对演示项目进行了一些微小的更改,值得查看并审查JVViewController didSelectRowAtIndexPath方法。


在JVCell中覆盖setAlpha:以阻止setAlpha:0似乎是一种解决方法,我没有看到任何负面影响,但它很丑陋。我会继续研究它,看看为什么它想要隐藏单元格。 - davehayden
我有一个类似的例子,你需要吗? - Dhara
如果它做的事情相同,当然可以。它需要具有背景图像,并在展开时更改单元格高度。它不能添加或删除单元格。它还必须具有动画效果。您可以将其发布在答案中并添加源代码或链接。你的代码有什么不同之处吗? - TigerCoding
12个回答

21
你的问题在 setExpanded 方法中:在 JVCell.m 文件中,你直接编辑了目标单元格的框架。
- (void)setExpanded:(BOOL)expanded
{
    expanded_ = expanded;

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

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

将其更新为:

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

然后在JVViewController.m的第163行中删除对-reloadRowsAtIndexPaths:withRowAnimation:的调用,它将按预期进行动画。
-reloadRowsAtIndexPaths:withRowAnimation:期望返回不同的单元格以供提供的indexPaths使用。由于您只是调整大小,-beginUpdates和-endUpdates足以重新布局表视图单元格。

2
顺便提一下,alpha 值被设置为零的原因是因为表格将一个单元格同时视为旧单元格和新单元格。表视图的行为是将旧单元格与其后面的新单元格淡出。因此,它的 alpha 值被设置为 1 作为“新”单元格,但随后从 1 淡出到零作为“旧”单元格。 - user349819
+1 - 已验证在模拟器中适用于4.3和5.1版本。 - Scott Marks
这个答案是最有道理的。如果可以自动改变单元格的高度,我宁愿不强制重新加载单元格。动画效果也很流畅。非常好!但使用beginUpdates和endUpdates是否是一种“hack”?我的代码是否正确使用了这些方法? - TigerCoding
这不是黑客行为,而是正确的做法。虽然参考文献没有明确说明,但 beginUpdates/endUpdates 的一个功能之一是将动画显示为新布局。beginUpdates/endUpdates 是唯一会从委托中调用信息(其中包括 rowHeight)并根据更改进行动画处理而无需重新加载单元格的方法。唯一可行的替代方法是调用 reloadData,并自己从一个状态到另一个状态执行所有动画,但我个人会避免这样做。 - user349819
看起来你的答案最适合,尽管@Deniz也提供了使其工作的方法。感谢您在此方面的帮助,我几乎以为它无法解决。现在我的表视图看起来非常好。再次感谢! - TigerCoding
没问题,谢谢你提供源代码。没有它,我会很难找到问题所在。 - user349819

10
也许我有所疏漏,但是为什么不直接使用:UITableViewRowAnimationNone,而不是:
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationNone];

想到所有这些麻烦都是为了这个问题。这是我对此问题的第二个赏金,我开始觉得它不可能被解决。剩下的问题是:为什么这个方法有效,以及我是否应该使用begin/end更新方法?谢谢,Deniz。 - TigerCoding
这个解决了我的问题。 - Kof

9

我很高兴滚动页面到底部看到那个答案 :) - lukasz
你能解释一下为什么应该使用beginupdates吗?此外,这并没有解释为什么reloadRow会使单元格消失@.@ - Happiehappie
谢谢,这个对我有用,应该被接受为答案! - Vrutin Rathod
@Happiehappie https://developer.apple.com/documentation/uikit/uitableview/1614935-reloadrowsatindexpaths 上说:“重新加载行会导致表视图向其数据源请求该行的新单元格。表格将新单元格动画显示出来,同时将旧行动画移出。如果您想要提醒用户单元格的值正在更改,请调用此方法。但是,如果通知用户并不重要——也就是说,您只想更改单元格正在显示的值——则可以获取特定行的单元格并设置其新值。” - CyberMew
@Happiehappie 不好意思,我应该先介绍一下 https://developer.apple.com/documentation/uikit/uitableview/1614908-beginupdates 这个方法,它说:“您还可以使用此方法,然后跟随 endUpdates() 方法来在不重新加载单元格的情况下动画更改行高度。” - CyberMew

5
正在消失的单元格是不改变大小的上一个单元格。正如reloadRowsAtIndexPaths:withRowAnimation:的文档所述:

该表在将旧行动画显示出来的同时,以动画形式显示新单元格。

发生的情况是将不透明度设置为1,然后立即设置为0,因此它会淡出。
如果之前和新的单元格都改变了大小,则它可以按预期工作。这是因为开始/结束更新通知高度更改并在这些单元格上创建新动画,覆盖了reloadRowsAtIndexPaths: withRowAnimation:中的动画。
您的问题是由于滥用reloadRowsAtIndexPaths: withRowAnimation:来调整单元格的大小,而它实际上是用于加载新单元格的。
但是,您根本不需要使用reloadRowsAtIndexPaths:withRowAnimation:。只需更改单元格的展开状态,然后执行开始/结束更新操作。这将为您处理所有动画。
作为副注,我发现蓝色选择有点烦人,在JVCell中将selectedBackgroundView设置为与backgroundView相同的图像(或创建一个具有所选单元格正确外观的新图像)即可解决。
编辑:
将添加previousIndexPath_indicesToReload的语句移动到if语句中(位于第132行),这样只有在之前的单元格已展开且需要调整大小时,才会添加它。
if( expanded ) {
    [previousCell setExpanded:NO];
    [indicesToReload addObject:[previousIndexPath_ copy]];
}

这样做可以避免之前折叠的单元格消失的情况。

另一个选项是在当前单元格折叠时将previousIndexPath_设置为nil,只有在单元格展开时才设置它。

这仍然感觉像一个hack。同时执行reloadRows和begin/end Updates会导致tableView两次重新加载所有内容,但两者都似乎需要正确地进行动画处理。我想如果表格不太大,这不会成为性能问题。


我尝试在JVViewController(reloadRowsForIndexPaths)中注释掉第149行,但高度变化仍然不能平滑地动画。注意当点击第二个单元格时,前一个单元格会有点“弹回”到原位?你可以看到表视图背景在单元格之间短暂出现。如何避免这种情况发生?关于蓝色,我同意并在JVCell setupImageView方法中添加了几行代码,以将选定的背景图像设置为相同的图像。这只是一个演示项目,完整的项目将使用不同的图形。下载更新后的项目。 - TigerCoding
我明白你所说的关于中心动画的问题。很奇怪的是,动画时间取决于表格中的位置。我在每个部分创建了更多的单元格,所有中心单元格的行为都是相同的。只有顶部和底部的单元格才能正确地进行动画。 - Nathan Kinsinger

2
简短实用的答案:将UITableViewRowAnimationAutomatic更改为UITableViewRowAnimationTop即可解决问题。没有更多消失的行了!(在iOS 5.1上测试过)
另一个简短实用的答案,因为据说UITableViewRowAnimationTop会引起自身的问题:创建一个新的单元格视图而不是修改现有的单元格视图框架。在真实的应用程序中,显示在单元格视图中的数据应该位于应用程序的模型部分,因此如果正确设计,则创建另一个单元格视图以不同的方式显示相同的数据(在我们的情况下为框架)不应该是问题。
关于动画重新加载相同单元格的一些其他想法:
在某些情况下,UITableViewRowAnimationAutomatic似乎会解析为UITableViewRowAnimationFade,这就是当您看到单元格淡出并消失时的情况。新单元格应该淡入,而旧单元格则淡出。但是,这里的旧单元格和新单元格是相同的-那么,这甚至能行吗?在核心动画级别上,是否可能同时淡出视图并淡入视图?听起来很可疑。因此结果是您只会看到淡出。这可能被认为是Apple的一个错误,因为预期的行为可能是如果相同的视图已更改,则不会动画化alpha属性(因为无法同时将其动画化为0和1),而只会动画化框架、颜色等。
请注意,问题仅在动画显示中出现-如果您滚动并返回,一切都会正确显示。
在iOS 4.3中,Automatic模式可能已被解析为其他内容,而不是Fade,这就是为什么它们可以正常工作的原因(正如您所写的那样)-我没有深入研究过。
我不知道iOS为什么选择Fade模式。但其中之一是当您的代码请求重新加载以前点击的单元格时,该单元格已折叠,并且与当前点击的单元格不同。请注意,以前点击的单元格始终会重新加载,您的代码中的此行始终被调用:
[indicesToReload addObject:[previousIndexPath_ copy]];

这解释了你所描述的神奇消失单元格的情况。

顺便说一下,beginUpdates/endUpdates 对我来说好像是一个hack。这对调用只是应该包含动画,而且除了你已经要求重新加载的行之外,没有添加任何动画。在这种情况下,它所做的就是以神奇的方式使 Automatic 模式在某些情况下不选择 Fade - 但这只是掩盖了问题。

最后注意一点:我尝试使用 Top 模式并发现它也可能会引起问题。例如,插入以下代码会使单元格奇怪地消失:

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

不确定这里是否存在真正的问题(类似于同时淡入淡出视图的问题),或者可能是苹果的一个 bug。


@Javy 更新了答案,提供了另一种解决方案 - 在更改框架时使用不同的单元格。顺便说一下 - 你写道你不感兴趣调用-reloadData,但是每次tableView为所有单元格重新显示时都会调用-heightForRowAtIndexPath:方法,在你的实现中这会调用-cellForRowAtIndexPath:。因此,所有单元格都将被重新检索,我认为这就是reloadData所做的大部分工作。 - Danra
另一个简短而实用的答案,因为UITableViewRowAnimationTop据说会引起自身的问题:创建一个新的单元格视图,而不是修改现有单元格视图的框架。在真正的应用程序中,显示在单元格视图中的数据应该在应用程序的模型部分中,因此如果设计得当,在我们的情况下,创建另一个单元格视图来以不同的方式显示相同的数据不应该是一个问题。 - Danra
这根本不是黑客行为。begin/end更新由于某种未知原因而起作用,可能会在不同的iOS版本中出现问题(实际上,如果您只重复调用-reloadRowsAtIndexPaths:一次,它就会出现问题,请尝试一下),alpha实际上与正在进行的淡入淡出动画相矛盾,并且也很可能会出现问题。从一个单元格视图到另一个单元格视图的淡入淡出可能正是苹果工程师在实现该功能时所考虑的 - 重用相同的单元格是不寻常的。苹果和其他代码几乎总是从-cellForRowAtIndexPath返回一个新单元格。 - Danra
让我们在聊天中继续这个讨论。点击此处进入聊天室 - Danra
一个UITableViewCell只是一个UIView,如果你创建一个新的,它将成为一个不同的单元格。 - Danra
显示剩余5条评论

1

我刚刚下载了你的项目,发现在didSelectRowAtIndexPath代理方法中有这段代码,其中使用了reloadRowsAtIndexPaths方法。

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

为什么不试试这个,而不是上面那个呢?

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

我提出这个建议的原因是我相信reloadRowsAtIndexPaths:...只有在调用以下方法之间才能正常工作:
- (void)beginUpdates;
- (void)endUpdates;

除此之外,行为是未定义的,并且正如您发现的那样,相当不可靠。引用“iPhone OS表视图编程指南”中相关部分:

要批量插入和删除行和节的动画,请在由连续调用beginUpdates和endUpdates定义的动画块内调用插入和删除方法。如果您不在此块内调用插入和删除方法,则行和节索引可能无效。 beginUpdates...endUpdates块不可嵌套。

在块结束时,即在endUpdates返回后,表视图像往常一样查询其数据源和委托以获取行和节数据。因此,支持表视图的集合对象应更新以反映新添加或删除的行或节。

reloadSections:withRowAnimation:和reloadRowsAtIndexPaths:withRowAnimation:方法是iPhone OS 3.0中引入的与上述方法相关的方法。它们允许您请求表视图重新加载特定节和行的数据,而不是通过调用reloadData来加载整个可见表视图。

可能还有其他有效的原因,但让我再考虑一下,因为我也有你的代码,我可以试着调试一下。希望我们能找出问题所在...


如果您在reloadRowsAtIndexPaths之前放置beginUpdates,则在单击时立即消失单元格。将其放置在reloadRowsAtIndexPaths之后可以解决此问题。尝试测试项目并查看发生了什么。关于Apple文档,“要动画显示批量插入和删除行和部分...”我没有插入或删除行或部分,我只是更改单元格的高度,仅此而已。我正在使用reload,因此它知道检查其高度是否已更改。;) - TigerCoding
好的,让我保留这个答案。我会尝试一些东西并在此之后回复您... - Srikar Appalaraju

1
当您使用此方法时,必须确保您在主线程上。按照以下方式刷新UITableViewCell即可解决问题:
- (void) refreshTableViewCell:(NSNumber *)row
{
    if (![[NSThread currentThread] isMainThread])
    {
        [self performSelector:_cmd onThread:[NSThread mainThread]  withObject:row waitUntilDone:NO];
        return;
    }

    /*Refresh your cell here
     ...
     */

}

0

@Javy,我在测试你的应用程序时注意到了一些奇怪的行为。

在运行iPhone 5.0模拟器时,previousIndexpth_变量是

class NSArray(看起来是这样的)。以下是调试器输出:

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

    <JVSectionData: 0x6a61230>,

    <JVSectionData: 0x6a64920>,

    <JVSectionData: 0x6a66260>
    )

    (lldb) po [previousIndexPath_ class]

    (id) $7 = 0x0145cb64 __NSArrayI

而在iPhone 4.3模拟器中,它的类型是NSIndexPath

lldb) po [previousIndexPath_ class]

(id) $5 = 0x009605c8 NSIndexPath

(lldb) po previousIndexPath_

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

你知道这个问题吗?不确定这是否有帮助,但想让你知道。


0

我认为当单元格未展开时,您不应保留先前的indexPath, 尝试通过修改您选择的方法来实现,如下所示,它可以正常工作。

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

如果您在代码更改的情况下运行此演示,则动画会突然跳动,而不是平稳运行,并且在单元格之间可以看到背景的部分。 - TigerCoding
是的,在我发表最后一条评论之前,我刚刚尝试了您的代码更改。请注意,在模拟器中,某些单元格之间的空格不会同时进行动画处理,您可以看到它们之间的背景。看起来很糟糕。@Saltymule在上面给出了一个很好的答案,只调整单元格的大小而不是重新加载它们,这个方法效果非常好。 - TigerCoding

0

我遇到了一个类似的问题,当开关被激活时,我想要展开一个单元格,以显示单元格中通常隐藏的额外标签和按钮。我尝试了各种版本的reloadRowsAtPath,但都没有成功。最后,我决定在heightForRowAtIndexPath中添加一个条件来简化它:

    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
}

无论你想用什么代码触发单元格的展开,只需插入tableview.reloadData()即可。在我的情况下,当打开一个开关以指示重置密码时,就会触发它。

        @IBAction func resetPasswordSwitch(sender: AnyObject) {

        tableView.reloadData()
        }

使用这种方法没有延迟,没有明显的方式可以看到表格重新加载,而且单元格的展开是逐渐完成的,就像您期望的那样。希望这能帮助到某些人。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接