【问题标题】:reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling具有动态单元格高度的 UITableView 的 reloadData() 导致跳跃滚动
【发布时间】: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 我还是不能解决问题,有什么好的解决办法吗?
  • 你有没有找到解决这个问题的方法?我遇到的问题与您在视频中看到的完全相同。
  • 以下答案均不适合我。

标签: ios uitableview swift autolayout


【解决方案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】:

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

    1. 确实只有 iOS 8。
    2. 覆盖 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
                          }
                          

                          我有可扩展的单元格。

                          【讨论】:

                            猜你喜欢
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 2014-12-18
                            • 2016-08-16
                            • 1970-01-01
                            • 1970-01-01
                            • 2014-07-06
                            相关资源
                            最近更新 更多