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

我这里有一个简单的示例项目:

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

在这个项目中,UITableView有一个自定义的UITableViewCell。 在每个单元格中有3个包含标签的UIViews。

目标是在敲击时扩展单元格,然后在再次敲击时将其折叠。 单元格本身必须更改其高度以显示子视图。 插入或删除行是不可接受的。

演示项目几乎按预期工作。 事实上,在iOS 4.3中,它的工作非常完美。 然而,在iOS 5下,当行崩溃时,先前的单元格神奇地消失。

要重新创建问题,请在iOS 5的模拟器或设备中运行项目,然后点击第一个单元格以展开它。 然后再次点击该单元格以折叠它。 最后,直接点击它下面的单元格。 前一个消失了。

继续点击该部分中的每个单元格将导致所有单元格消失,到整个部分缺失的位置。

我也尝试使用reloadData而不是当前的设置,但这会破坏动画并且感觉有点像黑客。 reloadRowsAtIndexPaths应该可以工作,但问题是为什么不呢?

查看下面发生的事情的图像:

表出现:

表出现了

细胞扩展:

表扩展

细胞塌陷:

桌子倒塌了

细胞消失(当敲击下面的细胞时):

细胞消失了

不断重复,直到整个部分消失:

部分消失了

编辑:覆盖alpha是一个黑客,但有效。 这是另一个“黑客”修复它,但为什么它修复它?

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方法。

您的问题出在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; } 

然后在-reloadRowsAtIndexPaths:withRowAnimation:的第163行删除对-reloadRowsAtIndexPaths:withRowAnimation:的调用,它将按预期动画。

-reloadRowsAtIndexPaths:withRowAnimation:期望为提供的indexPaths返回不同的单元格。 由于您只resize-beginUpdates-endUpdates足以再次布局表视图单元格。

可能是我错过了一点,但为什么不用你:

UITableViewRowAnimationNone

我的意思是代替:

 [tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationAutomatic]; 

使用

 [tableView reloadRowsAtIndexPaths:indicesToReload withRowAnimation:UITableViewRowAnimationNone]; 

要为tableView的高度更改设置动画,只需调用即可。

 [tableView beginUpdates]; [tableView endUpdates]; 

不要调用reloadRowsAtIndexPaths:

请参阅选中时,是否可以在UITableViewCell上设置高度更改动画?

淡出的单元格是前一个不改变大小的单元格。 作为reloadRowsAtIndexPaths:withRowAnimation:的文档reloadRowsAtIndexPaths:withRowAnimation: states:

该表为新单元设置动画,因为它为旧行设置了动画。

会发生什么是不透明度设置为1然后立即设置为0,因此淡出。

如果先前和新单元格都改变了大小,那么它按预期工作。 这是因为开始/结束更新会注意到高度的变化,并在覆盖reloadRowsAtIndexPaths:withRowAnimation: ones的那些单元格上创建新的动画。

您的问题是由于滥用reloadRowsAtIndexPaths:withRowAnimation:在用于加载新单元格时调整单元格的大小。

但是你根本不需要reloadRowsAtIndexPaths:withRowAnimation: 只需更改单元格的展开状态,然后执行开始/结束更新。 这将为您处理所有动画。

作为旁注,我发现蓝色选择有点烦人,在JVCell中将selectedBackgroundView设置为与backgroundView相同的图像(或创建具有所选单元格的正确外观的新图像)。


编辑:

将添加previousIndexPath_的语句添加到indicesToReload到if语句(在第132行),以便仅在前一个单元格被展开并且需要resize时才添加它。

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

这消除了先前折叠的单元格将消失的情况。

另一种选择是在折叠当前单元格时将previousIndexPath_设置为nil,并且仅在单元格展开时将其设置为nil。

这仍然像一个黑客。 同时执行reloadRows和开始/结束更新会导致tableView重新加载所有内容两次,但似乎都需要正确设置动画。 我想如果表格不是太大,这不会是性能问题。

简短实用的答案:将UITableViewRowAnimationAutomatic更改为UITableViewRowAnimationTop解决了这个问题。 没有更多的消失行! (在iOS 5.1上测试)

另一个简短实用的答案,因为据说UITableViewRowAnimationTop会导致自己的问题:创建一个新的单元格视图而不是修改现有的单元格。 在真实的应用程序中,单元格视图中显示的数据应该在应用程序的模型部分中,因此如果设计得当,创建另一个仅以不同方式显示相同数据的单元格视图应该不成问题。 (在我们的例子中框架)。

关于动画重新加载同一个单元格的更多想法:

在某些情况下, UITableViewRowAnimationAutomatic似乎解析为UITableViewRowAnimationFade ,当你看到细胞逐渐消失并消失时。 新单元应该淡入,而旧单元淡出。 但是旧电池和新电池是同一个 – 所以,这甚至可以工作吗? 在核心动画级别,是否可以淡出视图并同时淡入它? 听起来很可疑。 结果就是你只看到了淡出。 这可能被认为是一个Apple错误,因为预期的行为可能是,如果相同的视图已更改,则alpha属性将不会被动画化(因为它不能同时为0和1设置动画),但相反,只有框架,颜色等将动画。

请注意,问题只出现在动画的显示中 – 如果您向前滚动并向后滚动,一切都会正确显示。

在iOS 4.3中, Automatic模式可能已被解析为Fade其他东西,这就是为什么事情在那里工作(当你写的时候) – 我没有深入研究。

我不知道为什么iOS会选择Fade模式。 但其中一个例子就是当你的代码重新加载一个先前被攻击的单元格时,该单元格被折叠,并且与当前的单击单元格不同。 请注意,以前的tapped单元格总是被重新加载,代码中的这一行总是被调用:

 [indicesToReload addObject:[previousIndexPath_ copy]]; 

这解释了您所描述的神奇消失的细胞情景。

顺便说一句,beginUpdates / endUpdates对我来说似乎是一个黑客攻击。 这对调用只是包含动画,除了你已经要求重新加载的行之外,你没有添加任何动画。 在这种情况下所做的一切都是神奇地导致Automatic模式在某些情况下不选择Fade – 但这只是模糊了问题。

最后一点:我玩过Top模式,发现它也会导致问题。 例如,插入以下代码会使单元格消失:

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

不确定这里是否存在真正的问题(类似于同时淡入和淡出视图的问题),或者可能是Apple漏洞。

我刚刚下载了你的项目,并在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来加载整个可见表视图。

可能有其他正当理由,但让我稍微考虑一下,因为我也有你的代码,我可以用它来捣乱。 希望我们应该弄明白……

使用此方法时,必须确保您在主线程上。 刷新UITableViewCell如下所示应该可以做到:

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

@Javy,我在测试你的应用时发现了一些奇怪的行为。

在iPhone 5.0模拟器上运行时,previousIndexpth_变量属于NSArray类(看起来像。)这里是调试器输出

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

(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 2个索引[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]; } 

此问题是由cellForRowAtIndexPath中的缓存单元格返回引起的。 reloadRowsAtIndexPaths期望从cellForRowAtIndexPath获取新的新单元格。 如果你这样做,你会没事的……不需要解决方法。

来自Apple doc:“重新加载行会导致表视图向其数据源询问该行的新单元格。”

我有一个类似的问题,我想在一个开关激活显示时扩展一个单元格,并且单元格中的额外标签和按钮通常在单元格处于默认高度时隐藏(44)。 我尝试了各种版本的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() } 

这种方法没有任何滞后,没有明显的方法可以看到表重新加载,并且销售的扩展是按照您的预期逐渐完成的。 希望这有助于某人。

试试这个希望它会对你的Cell扩展有所帮助