具有动态单元格高度的 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) 的自定义单元格中