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

时间:2015-01-30 20:56:24

标签: ios uitableview swift autolayout

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

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

我认为这是因为我重新加载数据,因为我向上滚动,UITableView重新计算每个进入可见性的单元格的高度。如何缓解这种情况,或者如何仅将数据从某个IndexPath重新加载到UITableView的末尾?

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

22 个答案:

答案 0 :(得分:164)

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

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary;

// initialize in code (thanks to @Gerharbo)
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 :(得分:86)

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 
}

答案 2 :(得分:32)

跳跃是因为估计的高度不好。 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
}

答案 3 :(得分:22)

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

答案 4 :(得分:11)

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

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

快捷键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()

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

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

并正常进行reloadData()

答案 5 :(得分:9)

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

  1. 确实只是iOS 8。
  2. 覆盖cellForRowAtIndexPath无济于事。
  3. 修复实际上非常简单:

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

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

    注意:我实际上知道我的细胞大小。只有两个可能的值。如果您的单元格确实是可变大小的,那么您可能希望从cell.bounds.size.height

    缓存tableView:willDisplayCell:forRowAtIndexPath:

答案 6 :(得分:6)

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

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

但是,通常情况下,您还可以为表格单元格高度更改设置动画,如下所示:

tableView.beginUpdates()
tableView.endUpdates()

答案 7 :(得分:4)

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

对于视图控制器:

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的扩展名

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)

答案 8 :(得分:1)

这里是一个简短的版本:

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

答案 9 :(得分:1)

这个在Swift4中为我工作:

extension UITableView {

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

答案 10 :(得分:0)

这些解决方案都不适合我。这是我对 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中的每个单元格提供一个估计的行高。尽可能准确...

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

答案 11 :(得分:0)

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

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
}

我有可扩展的单元格。

答案 12 :(得分:0)

对我来说,它适用于“heightForRowAt”

extension APICallURLSessionViewController: UITableViewDelegate {

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

答案 13 :(得分:0)

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

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

答案 14 :(得分:0)

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

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

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

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

答案 15 :(得分:0)

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

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

答案 16 :(得分: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

答案 17 :(得分:0)

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

这应该可以解决问题:)

答案 18 :(得分:0)

我有两种不同的细胞高度。

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

答案 19 :(得分:0)

我相信iOS11中引入了一个 bug

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

答案 20 :(得分:0)

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

答案 21 :(得分:-1)

实际上,我发现如果您使用reloadRows会导致跳转问题。然后,您应该尝试像这样使用reloadSections

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