UITableView分页加载时插入新单元格出现异常滚动动画,求优化方案
无缝UITableView分页实现(保留自定义Cell动画)
我之前也碰到过这个棘手的问题——用willDisplayCell触发分页后,插入新Cell导致TableView自动向上滚动,破坏了流畅的滚动体验。既要避免全量reloadData,又要保留自定义动画,核心是控制插入新数据后的滚动位置,同时做好状态管理防止重复加载。
核心思路
- 用一个加载状态标记避免重复触发分页请求;
- 插入新数据前记录当前的
contentOffset和contentSize; - 插入新行后,手动调整
contentOffset来抵消contentSize变化带来的滚动偏移; - 在
willDisplayCell中给新Cell添加自定义动画,同时避免Cell复用导致重复执行动画。
具体实现代码
1. 定义基础变量
首先在你的ViewController里定义必要的状态和数据源:
import UIKit class YourViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { // 数据源 private var dataSource: [YourDataModel] = [] // 分页加载状态标记,防止重复请求 private var isLoadingMore = false // TableView实例(假设你已经通过代码或Storyboard创建) private let tableView = UITableView() // ... 其他初始化代码 ... }
2. 实现willDisplayCell代理
这里处理自定义Cell动画和分页触发逻辑:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { // 给新Cell添加自定义动画(这里以淡入为例) guard let customCell = cell as? YourCustomCell, !customCell.hasAnimated else { return } customCell.alpha = 0.0 UIView.animate(withDuration: 0.3, delay: 0.1 * Double(indexPath.row % 3), options: .curveEaseOut) { customCell.alpha = 1.0 } customCell.hasAnimated = true // 触发分页:滚动到最后一个Cell且未在加载中 let lastSection = tableView.numberOfSections - 1 let lastRowInLastSection = tableView.numberOfRows(inSection: lastSection) - 1 if indexPath.section == lastSection && indexPath.row == lastRowInLastSection && !isLoadingMore { isLoadingMore = true fetchNextPageData() } }
注意:给自定义Cell添加一个hasAnimated属性,避免Cell复用时重复执行动画:
class YourCustomCell: UITableViewCell { // 标记是否已经执行过动画 var hasAnimated = false // ... 你的Cell内容和布局代码 ... }
3. 分页请求与插入新数据
这里是关键的滚动位置控制逻辑:
private func fetchNextPageData() { // 模拟网络请求(替换成你的实际API请求逻辑) DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in guard let self = self else { return } // 假设获取到了新数据 guard let newData = self.getNewPageData() else { self.isLoadingMore = false return } DispatchQueue.main.async { // 记录插入前的关键参数 let oldContentSizeHeight = self.tableView.contentSize.height let oldOffset = self.tableView.contentOffset // 计算新数据对应的IndexPath let startRow = self.dataSource.count self.dataSource.append(contentsOf: newData) let newIndexPaths = (startRow..<self.dataSource.count).map { IndexPath(row: $0, section: 0) } // 插入新行(用.none避免系统自带动画,我们自己在willDisplay里加) self.tableView.beginUpdates() self.tableView.insertRows(at: newIndexPaths, with: .none) self.tableView.endUpdates() // 调整滚动位置:抵消contentSize变化带来的偏移 let newContentSizeHeight = self.tableView.contentSize.height let heightDelta = newContentSizeHeight - oldContentSizeHeight let adjustedOffset = CGPoint(x: oldOffset.x, y: oldOffset.y + heightDelta) // 禁用动画设置偏移,保证无缝体验 self.tableView.setContentOffset(adjustedOffset, animated: false) // 重置加载状态 self.isLoadingMore = false } } }
关键细节说明
- 为什么要调整contentOffset?:插入新行后,TableView的
contentSize会变大,默认会自动调整滚动位置以保持当前可见内容的相对位置,这就是导致向上滚动的原因。我们手动调整偏移量,让滚动位置和插入前完全一致,实现无缝效果。 - 自动行高适配:如果你的TableView用了
UITableView.automaticDimension,必须在endUpdates()之后获取新的contentSize,因为只有此时系统才会计算出新Cell的高度。 - 加载状态控制:
isLoadingMore标记一定要加,否则用户快速滚动时会多次触发分页请求,导致数据重复插入或UI混乱。
内容的提问来源于stack exchange,提问作者Zakaria




