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


142

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

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

我相信这是由于以下事实:当我向上滚动时重新加载数据时,UITableView正在重新计算每个可见单元的高度。如何缓解这种情况,或者如何仅将数据从某个IndexPath重新加载到UITableView的末尾?

此外,当我确实设法一直滚动到顶部时,我可以先向下再向上滚动,毫无问题。这很可能是因为已经计算了UITableViewCell的高度。


有两件事...(1)是的,您绝对可以使用来重新加载某些行reloadRowsAtIndexPaths。但是(2)“跳跃”是什么意思,(3)是否设置了估计的行高?(只是想弄清楚是否有更好的解决方案,可以让您动态更新表格。)
Lyndsey Scott

@LyndseyScott,是的,我已经设置了估计的行高。跳动的意思是当我向上滚动时,行向上移动。我相信这是因为我将行的估计高度设置为128,然后在向上滚动时,UITableView中上面的所有帖子都变小了,因此它缩小了高度,导致我的表跳了起来。我正在考虑在TableView中从行x到最后一行执行reloadRowsAtIndexPaths ...但是由于我要插入新行,因此无法正常工作,在重新加载之前我不知道tableview的结尾是什么数据。
大卫

2
@LyndseyScott我还是无法解决问题,有什么好的解决方法吗?
rad 2015年

1
您是否找到解决此问题的方法?我遇到了与您的视频完全相同的问题。
user3344977 2015年

1
以下答案对我都不起作用。
Srujan Simha

Answers:


220

为防止跳跃,应在加载单元格时保留它们的高度,并在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;
}

1
谢谢,您真的节省了我的时间:)也可以在objc中使用
Artem Z.

3
别忘了初始化cellHeightsDictionarycellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo '16

1
estimatedHeightForRowAtIndexPath:返回双精度值可能会导致*** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]错误。要修复它,请return floorf(height.floatValue);改为。
liushuaikobe

嗨,@ lgor,我遇到了同样的问题,并尝试实施您的解决方案。我遇到的问题是在willDisplayCell之前调用了EstimateHeightForRowAtIndexPath,因此,在调用EstimateHeightForRowAtIndexPath时不会计算单元格的高度。有帮助吗?
Madhuri

1
@Madhuri有效高度应在“ heightForRowAtIndexPath”中计算,即在屏幕上将在willDisplayCell之前调用的每个单元格,它将在字典中设置高度,以供以后在估计的行高度中使用(在重新加载表格时)。
Donnit

108

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 {,这可以处理我需要的所有高度计算。
Natalia

UITableViewDelegate坚持跳下奋斗了许多小时后,我发现自己忘了加入课堂。必须遵守该协议,因为它包含上面显示的willDisplay功能。我希望我可以挽救同样的人。
MJQZ1347

谢谢您的迅速回答。在我的情况下,当表格视图滚动到底部附近时,在重新加载时,单元格出现了一些奇怪的异常行为。从现在开始,每当我有自动调整大小的单元格时,都将使用它。
Trev14

在Swift 4.2
Adam S中

救生员。在尝试在数据源中添加更多项时非常有用。防止新添加的单元格跳到屏幕中央。
菲利普·博邦

38

跳跃是由于估计的高度不佳。估计的行高度与实际高度的差异越大,表在重新加载时可能会跳得越多,尤其是滚动到更低的位置时。这是因为表的估计大小与实际大小完全不同,从而迫使表调整其内容大小和偏移量。因此,估计高度不应为随机值,而应接近您认为的高度。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
}

2
谢谢,这就是为什么我的tableView如此跳动的原因。
路易·德·德克

1
这是一个旧的答案,但是到2018年仍然是实际的。与所有其他答案不同,该答案建议在viewDidLoad中设置一次一次EstimatedRowHeigh,这在单元格高度相同或非常相似时会有所帮助。谢谢 顺便说一句,也可以通过Interface Builder在Size Inspector> Table View> Estimate中设置esimatedRowHeight。
Vitalii

提供了更准确的估计身高帮助了我。我也有一个多部分的分组表格视图样式,并且必须实现tableView(_:estimatedHeightForHeaderInSection:)
nteissler

25

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

6
如何使用此解决方案处理行插入/删除?TableView会跳转,因为字典数据不是实际的。
阿列克谢·切卡诺夫

1
很棒!特别是在重新加载行时的最后一个单元格上。

19

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

花了几个小时并经历了所有可能的挫折之后,找到了解决此问题的方法。此解决方案是救生员!像魅力一样工作!

斯威夫特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()


1
如何重新加载?你叫它reloadWithoutAnimation但哪里reload呢?
马特

您可以tableView.reloadData()先调用@matt ,然后再调用tableView.reloadWithoutAnimation(),它仍然有效。
Srujan Simha

大!以上都不对我没有帮助。即使所有的高度和估计的高度也完全相同。有趣。
TY Kucuk '18

1
不要为我工作 在tableView.endUpdates()崩溃。有人能帮我吗!
卡卡西

12

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

对于视图控制器:

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 reloadSectionWithouAnimation(section: Int) {
      UIView.performWithoutAnimation {
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      }
  }
}

结果是

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
对我来说,关键是在这里实现他的UITableView扩展。非常聪明。感谢rastislv
BennyTheNerd '19

效果很好,但是它只有一个缺点,插入页眉,页脚或行会丢失动画。
苏富安·侯赛姆(Soufian Hossam),

reloadSectionWithouAnimation在哪里被调用?因此,例如,用户可以在我的应用程序(如Instagram)中发布图片;我可以调整图像的大小,但是在大多数情况下,必须将表格单元格从屏幕上滚动下来才能实现。我希望表格通过reloadData后,单元格的大小正确。
路加·欧文

11

我今天遇到了这个问题,观察到:

  1. 确实只有iOS 8。
  2. 覆写cellForRowAtIndexPath无济于事。

修复实际上非常简单:

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

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

注意:我实际上知道我的细胞大小。只有两个可能的值。如果您的单元格确实是可变大小的,那么您可能想要缓存cell.bounds.size.heightfromtableView:willDisplayCell:forRowAtIndexPath:


2
修复了它以较高的值(例如300f
Flappy 16'May

1
@Flappy,您提供的解决方案的工作方式很有趣,并且比其他建议的技术还短。请考虑将其发布为答案。
Rohan Sanap

9

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

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

但是,通常,您还可以像这样对表格单元格高度变化进行动画处理:

tableView.beginUpdates()
tableView.endUpdates()

我尝试了beginUpdates / endUpdates方法,但这仅影响表的可见行。向上滚动时仍然有问题。
大卫

@David可能是因为您使用的是估计的行高。
林德西·斯科特

我应该摆脱我的EstimatedRowHeights,而是将其替换为beginUpdates和endUpdates吗?
大卫

@David您不会“替换”任何东西,但这实际上取决于所需的行为...如果您想使用估计的行高并仅将索引重新加载到表的当前可见部分下方,则可以执行以下操作我说过使用reloadRowsAtIndexPaths
Lyndsey Scott

尝试reladRowsAtIndexPaths方法的问题之一是我正在实现无限滚动,所以当我重新加载Data时,这是因为我刚刚向dataSource添加了15行。这意味着这些行的indexPaths尚未存在于UITableView中
David

3

这是一个简短的版本:

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

3

用较高的值(例如300f)覆盖estimatedHeightForRowAtIndexPath方法

这应该可以解决问题:)


2

我相信有一个错误是在iOS11中引入的。

那就是当您执行reloadtableView 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)

这个答案是从这里得到的


2

这个在Swift4中为我工作:

extension UITableView {

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

1

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

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

tableView.rowHeight = UITableView.automaticDimension

同样在viewDidLoad()中,将所有tableView单元格笔尖注册到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的每个单元格提供一个估计的行高,estimateHeightForRowAt。尽可能准确...

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


1

我有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)
}

0

cell.layoutSubviews()在传回之前,请尝试致电func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?。这是iOS8中的已知错误。


0

您可以在以下位置使用 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

0

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

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


0

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

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

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

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


0

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

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
   UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()

-2

实际上,我发现您是否使用过reloadRows引起跳转问题。然后,您应该尝试reloadSections像这样使用:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.