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

我觉得这可能是一个共同的问题,并想知道是否有任何共同的解决办法。

基本上,我的 UITableView 对每个单元格都有动态单元格高度。如果我不在 UITableView 和 tableView.reloadData()的顶部,向上滚动就会变得神经质。

我相信这是因为当我向上滚动时重新加载了数据,UITableView 正在重新计算每个可见单元格的高度。如何减轻这种影响,或者如何只从某个 IndexPath 重新加载 Data 到 UITableView 的末尾?

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

54222 次浏览

实际上,通过使用 reloadRowsAtIndexPaths,您只能重新加载某些行,例如:

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

但是,一般情况下,您也可以像下面这样动画表单元格高度的变化:

tableView.beginUpdates()
tableView.endUpdates()

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

我今天偶然发现了这个:

  1. 确实只有 iOS8。
  2. 重写 cellForRowAtIndexPath也没用。

解决办法其实很简单:

重写 estimatedHeightForRowAtIndexPath并确保它返回正确的值。

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

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

跳跃是因为一个错误的估计高度。估计的 RowHeight 与实际高度的差异越大,表在重新加载时可能跳转的次数就越多,特别是在向下滚动时。这是因为表的估计大小与其实际大小完全不同,迫使表调整其内容大小和偏移量。 所以估计的高度不应该是一个随机值,而应该接近你认为的高度。我也有经验,当我设置 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: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
}

目标丙:

// 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;
}

迅捷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
}

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

我已经尝试了上面所有的变通方法,但是都不管用。

在花了几个小时,经历了所有可能的挫折之后,找到了解决这个问题的方法。这个解决方案是一个生命救世主!非常有效!

Swift 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()

下面是一个简短的版本:

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

有一个 臭虫,我相信是在 iOS11中引入的。

也就是说,当您执行 reload操作时,tableViewcontentOffSet会发生意外的改变。事实上 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)

这个答案来自 给你

这个在 Swift4中对我很有用:

extension UITableView {


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

这些解决方案对我都不管用。下面是我对 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

我有两个不同的牢房高度。

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

在我加入 估计高度为 Rowat之后,就没有跳跃了。

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

使用高值(例如300f)重写 EstiatedHeightForRowAtIndexPath 方法

这应该能解决问题:)

我用了更多的方法来解决这个问题:

视图控制器:

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)

您可以在 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

我有这种跳跃行为,我最初能够减轻它通过设置精确的估计标题高度(因为我只有1个可能的标题视图) ,然而,跳跃然后开始发生 在里面的标题具体,不再影响整个表。

根据这里的答案,我得到了它与动画相关的线索,所以我发现表视图在堆栈视图中,有时我们会在动画块中调用 stackView.layoutIfNeeded()。我的最终解决方案是确保这个调用不会发生,除非“真正”需要,因为布局“如果需要”有视觉行为在该上下文中,即使“不需要”。

我也有同样的问题。我有分页和重新加载数据没有动画,但它没有帮助滚动防止跳转。我有不同尺寸的 iPhone,iphone8上的滚动条不跳跃,但 iphone7 + 上的滚动条跳跃

我对 ViewDidLoad的功能进行了以下更改:

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

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

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

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

实际上,我发现如果你使用 reloadRows会导致一个跳跃问题,那么你应该试着像这样使用 reloadSections:

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

对我来说,它与“ heightForRowAt”一起工作

extension APICallURLSessionViewController: UITableViewDelegate {


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

对我来说,可行的解决办法是

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
}

我有可膨胀的细胞。