具有动态单元格高度的 UITableView 的 reloadData() 导致跳跃滚动

Posted

技术标签:

【中文标题】具有动态单元格高度的 UITableView 的 reloadData() 导致跳跃滚动【英文标题】:reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling 【发布时间】:2015-03-30 10:27:10 【问题描述】:

我觉得这可能是一个常见问题,想知道是否有任何常见的解决方案。

基本上,我的 UITableView 具有每个单元格的动态单元格高度。如果我不在 UITableView 的顶部并且我 tableView.reloadData(),向上滚动会变得跳跃。

我相信这是因为我重新加载了数据,当我向上滚动时,UITableView 正在重新计算每个单元格的高度,以使其可见。如何减轻这种情况,或者如何仅将某个 IndexPath 中的数据重新加载到 UITableView 的末尾?

此外,当我设法一直滚动到顶部时,我可以向下滚动然后向上滚动,没有跳跃的问题。这很可能是因为 UITableViewCell 的高度已经计算过了。

【问题讨论】:

几件事... (1) 是的,您绝对可以使用reloadRowsAtIndexPaths 重新加载某些行。但是(2)“跳跃”是什么意思,(3)您是否设置了估计的行高? (只是想弄清楚是否有更好的解决方案可以让您动态更新表格。) @LyndseyScott,是的,我已经设置了估计的行高。跳跃是指当我向上滚动时,行向上移动。我相信这是因为我将估计的行高设置为 128,然后当我向上滚动时,我在 UITableView 上面的所有帖子都变小了,所以它缩小了高度,导致我的表格跳了起来。我正在考虑从行 x 到 TableView 中的最后一行执行 reloadRowsAtIndexPaths ......但是因为我要插入新行,所以它不起作用,我不知道我的 tableview 的结尾是什么在我重新加载数据之前。 @LyndseyScott 我还是不能解决问题,有什么好的解决办法吗? 你有没有找到解决这个问题的方法?我遇到的问题与您在视频中看到的完全相同。 以下答案均不适合我。 【参考方案1】:

您实际上可以使用reloadRowsAtIndexPaths 重新加载某些行,例如:

tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

但是,一般来说,您也可以像这样为表格单元格的高度变化设置动画:

tableView.beginUpdates()
tableView.endUpdates()

【讨论】:

我已经尝试过 beginUpdates/endUpdates 方法,但这只会影响我表的可见行。当我向上滚动时,我仍然遇到问题。 @David 可能是因为您使用的是估计的行高。 我应该摆脱我的 EstimatedRowHeights,而是用 beginUpdates 和 endUpdates 替换它吗? @David 您不会“替换”任何东西,但这实际上取决于所需的行为...如果您想使用估计的行高并重新加载当前可见部分下方的索引表,你可以像我说的那样使用 reloadRowsAtIndexPaths 我尝试 reladRowsAtIndexPaths 方法的一个问题是我正在实现无限滚动,所以当我重新加载数据时,这是因为我刚刚向数据源添加了 15 行。这意味着这些行的 indexPaths 在 UITableView 中尚不存在【参考方案2】:

在返回func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell? 中的单元格之前尝试调用cell.layoutSubviews()。这是ios8中的已知错误。

【讨论】:

【参考方案3】:

我今天遇到这个并观察到:

    确实只有 iOS 8。 覆盖 cellForRowAtIndexPath 没有帮助。

修复其实很简单:

覆盖 estimatedHeightForRowAtIndexPath 并确保它返回正确的值。

这样,我的 UITableViews 中所有奇怪的抖动和跳跃都停止了。

注意:我实际上知道我的细胞的大小。只有两个可能的值。如果您的单元格是真正可变大小的,那么您可能希望缓存来自tableView:willDisplayCell:forRowAtIndexPath:cell.bounds.size.height

【讨论】:

修复了用高值覆盖estimateHeightForRowAtIndexPath方法时的问题,例如300f @Flappy 有趣的是,您提供的解决方案是如何工作的,并且比其他建议的技术更短。请考虑将其发布为答案。【参考方案4】:

跳跃是因为估计的高度错误。 estimatedRowHeight 与实际高度的差异越大,重新加载时表格可能会跳得越多,尤其是向下滚动的地方越多。这是因为表格的估计大小与其实际大小完全不同,迫使表格调整其内容大小和偏移量。 所以估计的高度不应该是一个随机值,而是接近你认为的高度。我设置UITableViewAutomaticDimension的时候也经历过 如果您的单元格类型相同,则

func viewDidLoad() 
     super.viewDidLoad()
     tableView.estimatedRowHeight = 100//close to your cell height

如果你在不同的部分有不同的单元格,那么我认为更好的地方是

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat 
     //return different sizes for different cells if you need to
     return 100

【讨论】:

谢谢,这正是我的 tableView 如此跳跃的原因。 一个旧答案,但截至 2018 年它仍然是实际的。与所有其他答案不同,这个答案建议在 viewDidLoad 中设置一次估计行高度,这有助于当单元格具有相同或非常相似的高度时。谢谢。顺便说一句,也可以通过 Interface Builder 在 Size Inspector > Table View > Estimate 中设置 esimatedRowHeight。 提供了更准确的估计身高帮助我。我还有一个多节分组表视图样式,并且必须实现tableView(_:estimatedHeightForHeaderInSection:)【参考方案5】:

为防止跳跃,您应该在加载单元格时保存它们的高度,并在tableView:estimatedHeightForRowAtIndexPath 中给出准确的值:

斯威夫特:

var cellHeights = [IndexPath: CGFloat]()

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
    cellHeights[indexPath] = cell.frame.size.height


func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 
    return cellHeights[indexPath] ?? UITableView.automaticDimension

目标 C:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = @.mutableCopy;

// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;

// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath 
    [cellHeightsDictionary setObject:@(cell.frame.size.height) forKey:indexPath];


// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath 
    NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
    if (height) return height.doubleValue;
    return UITableViewAutomaticDimension;

【讨论】:

谢谢,你真的拯救了我的一天 :) 也可以在 objc 中使用 别忘了初始化cellHeightsDictionary:cellHeightsDictionary = [NSMutableDictionary dictionary]; estimatedHeightForRowAtIndexPath: 返回双精度值可能会导致 *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:] 错误。要修复它,请改用return floorf(height.floatValue); @Madhuri 有效高度应在 "heightForRowAtIndexPath" 中计算,即在 willDisplayCell 之前为屏幕上的每个单元格调用,这将在字典中设置高度以供以后在estimatedRowHeight 中使用(在表重新加载时)。 您应该如何使用此解决方案处理行插入/删除? TableView 跳转,因为字典数据不是真实的。【参考方案6】:

已接受答案的 Swift 3 版本。

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
    cellHeights[indexPath] = cell.frame.size.height


func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 
    return cellHeights[indexPath] ?? 70.0 

【讨论】:

谢谢,效果很好!事实上,我能够删除我对func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 的实现,它可以处理我需要的所有高度计算。 在坚持跳跃数小时后,我发现我忘记在课堂上添加UITableViewDelegate。必须遵守该协议,因为它包含上面显示的willDisplay 函数。我希望我能拯救同样挣扎的人。 感谢您的快速回答。在我的情况下,当表格视图滚动到/接近底部时,我在重新加载时出现了一些超级奇怪的单元格行为。从现在开始,只要我有自调整大小的单元格,我就会使用它。 在 Swift 4.2 中完美运行 这是一个很好的答案——我唯一的建议是将estimatedHeightForRowAt: 方法中的默认值替换为UITableView.automaticDimension。这样,它将回退到(通常不精确但希望接近)Apple 自动确定的值,而不是 70。【参考方案7】:

@Igor 答案在这种情况下工作正常,Swift-4 代码。

// declaration & initialization  
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]  

UITableViewDelegate的以下方法中

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
  // print("Cell height: \(cell.frame.size.height)")
  self.cellHeightsDictionary[indexPath] = cell.frame.size.height


func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 
  if let height =  self.cellHeightsDictionary[indexPath] 
    return height
  
  return UITableView.automaticDimension

【讨论】:

如何使用此解决方案处理行插入/删除? TableView 跳转,因为字典数据不是真实的。 效果很好!尤其是在重新加载行时的最后一个单元格。【参考方案8】:

我已经尝试了上述所有解决方法,但没有任何效果。

在花费了数小时并经历了所有可能的挫折之后,找到了解决此问题的方法。这个解决方案是救命稻草!像魅力一样工作!

斯威夫特 4

let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)

我将它作为扩展添加,以使代码看起来更干净,并避免每次我想重新加载时都写所有这些行。

extension UITableView 

    func reloadWithoutAnimation() 
        let lastScrollOffset = contentOffset
        beginUpdates()
        endUpdates()
        layer.removeAllAnimations()
        setContentOffset(lastScrollOffset, animated: false)
    

终于..

tableView.reloadWithoutAnimation()

或者您实际上可以在 UITableViewCell awakeFromNib() 方法中添加这些行

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

然后正常reloadData()

【讨论】:

如何重新加载?您它为reloadWithoutAnimation,但reload 部分在哪里? @matt 你可以先调用tableView.reloadData(),然后再调用tableView.reloadWithoutAnimation(),它仍然有效。 太棒了!以上都不适合我。甚至所有的高度和估计的高度都完全一样。很有趣。 不要为我工作。它在 tableView.endUpdates() 处崩溃。谁能帮帮我!【参考方案9】:

这里有一个更短的版本:

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 
    return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension

【讨论】:

【参考方案10】:

我相信 iOS11 中引入了一个 错误

那是当您执行reload 时,tableView contentOffSet 会意外更改。事实上contentOffset 在重新加载后不应该改变。这往往是由于UITableViewAutomaticDimension的错误计算而发生的

您必须保存您的contentOffSet 并在重新加载完成后将其设置回您保存的值。

func reloadTableOnMain(with offset: CGPoint = CGPoint.zero)

    DispatchQueue.main.async  [weak self] () in

        self?.tableView.reloadData()
        self?.tableView.layoutIfNeeded()
        self?.tableView.contentOffset = offset
    

你如何使用它?

someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)

这个答案来自here

【讨论】:

【参考方案11】:

这个在 Swift4 中为我工作:

extension UITableView 

    func reloadWithoutAnimation() 
        let lastScrollOffset = contentOffset
        reloadData()
        layoutIfNeeded()
        setContentOffset(lastScrollOffset, animated: false)
    

【讨论】:

【参考方案12】:

这些解决方案都不适合我。以下是我对 Swift 4 和 Xcode 10.1 所做的...

在 viewDidLoad() 中,声明表格动态行高并在单元格中创建正确的约束...

tableView.rowHeight = UITableView.automaticDimension

同样在 viewDidLoad() 中,将所有 tableView 单元格 nib 注册到 tableview,如下所示:

tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")

在tableView heightForRowAt中,返回等于indexPath.row中每个单元格高度的高度...

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 

    if indexPath.row == 0 
        let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
        return cell.layer.frame.height
     else if indexPath.row == 1 
        let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
        return cell.layer.frame.height
     else 
        let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
        return cell.layer.frame.height
     


现在给 tableView 中每个单元格的估计行高估计值HeightForRowAt。尽可能准确...

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 

    if indexPath.row == 0 
        return 400 // or whatever YourTableViewCell's height is
     else if indexPath.row == 1 
        return 231 // or whatever YourSecondTableViewCell's height is
     else 
        return 216 // or whatever YourThirdTableViewCell's height is
     


应该可以的...

调用tableView.reloadData()时不需要保存和设置contentOffset

【讨论】:

【参考方案13】:

我有 2 个不同的单元格高度。

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 
        let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
        return Helper.makeDeviceSpecificCommonSize(cellHeight)
    

在我添加 estimatedHeightForRowAt 之后,就不再跳跃了。

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 
    let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
    return Helper.makeDeviceSpecificCommonSize(cellHeight)

【讨论】:

【参考方案14】:

用高值覆盖estimateHeightForRowAtIndexPath方法,例如300f

这应该可以解决问题:)

【讨论】:

这对我有用,但问题是为什么?【参考方案15】:

我使用了更多的方法来解决它:

对于视图控制器:

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
    cellHeights[indexPath] = cell.frame.size.height


func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 
    return cellHeights[indexPath] ?? 70.0 

作为 UITableView 的扩展

extension UITableView 
  func reloadSectionWithoutAnimation(section: Int) 
      UIView.performWithoutAnimation 
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      
  

结果是

tableView.reloadSectionWithoutAnimation(section: indexPath.section)

【讨论】:

对我来说关键是在这里实现他的 UITableView 扩展。非常聪明。谢谢rastislv 完美运行,但它只有一个缺点,插入页眉、页脚或行时会丢失动画。 reloadSectionWithouAnimation 会在哪里调用?因此,例如,用户可以在我的应用程序(如 Instagram)中发布图像;我可以让图像调整大小,但在大多数情况下,我必须将表格单元格滚动到屏幕上才能发生这种情况。一旦表格通过 reloadData,我希望单元格的大小正确。【参考方案16】:

您可以在ViewDidLoad()中使用以下内容

tableView.estimatedRowHeight = 0     // if have just tableViewCells <br/>

// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0

【讨论】:

【参考方案17】:

我有这种跳跃行为,我最初能够通过设置准确的估计标题高度来缓解它(因为我只有 1 个可能的标题视图),但是跳跃随后开始发生在 内部特别是标题,不再影响整个表格。

按照这里的答案,我知道它与动画有关,所以我发现表格视图位于堆栈视图内,有时我们会在动画块内调用stackView.layoutIfNeeded()。我的最终解决方案是确保除非“确实”需要,否则不会发生此调用,因为“如果需要”布局在该上下文中具有视觉行为,即使“不需要”也是如此。

【讨论】:

【参考方案18】:

我有同样的问题。我在没有动画的情况下进行了分页和重新加载数据,但它并没有帮助滚动防止跳跃。我有不同尺寸的 iPhone,滚动在 iphone8 上不跳动,但在 iphone7+ 上跳动

我对 viewDidLoad 函数应用了以下更改:

    self.myTableView.estimatedRowHeight = 0.0
    self.myTableView.estimatedSectionFooterHeight = 0
    self.myTableView.estimatedSectionHeaderHeight = 0

我的问题解决了。希望对你也有帮助。

【讨论】:

【参考方案19】:

我发现解决这个问题的方法之一是

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock 
   UIView.setAnimationsEnabled(true)

tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()

【讨论】:

【参考方案20】:

其实我发现如果你使用reloadRows会导致跳转问题。那么你应该尝试像这样使用reloadSections

UIView.performWithoutAnimation 
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)

【讨论】:

【参考方案21】:

对我来说,它与“heightForRowAt”一起使用

extension APICallURLSessionViewController: UITableViewDelegate 

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 
    print("Inside heightForRowAt")
    return 130.50


【讨论】:

【参考方案22】:

对我来说,可行的解决方案是

UIView.setAnimationsEnabled(false)
    tableView.performBatchUpdates  [weak self] in
    self?.tableView.reloadRows(at: [indexPath], with: .none)
 completion:  [weak self] _ in
    UIView.setAnimationsEnabled(true)
    self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) // remove if you don't need to scroll

我有可扩展的单元格。

【讨论】:

以上是关于具有动态单元格高度的 UITableView 的 reloadData() 导致跳跃滚动的主要内容,如果未能解决你的问题,请参考以下文章

UITableView:如何知道 TableView 的所有具有动态单元格高度的单元格何时被调整大小?

设置 tableviewcell 高度以显示具有动态高度单元格的完整内部/嵌套 uitableview

实现 UITableView (Slave) 的动态高度,它作为子视图添加到另一个具有动态高度的 UITableView (Master) 的自定义单元格中

具有自动布局的 UITableView 中的动态单元格高度

动态 UITableView 单元格高度的问题

UITableView - 由单元格本身控制的动态单元格高度