You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

UITableView分页加载时插入新单元格出现异常滚动动画,求优化方案

无缝UITableView分页实现(保留自定义Cell动画)

我之前也碰到过这个棘手的问题——用willDisplayCell触发分页后,插入新Cell导致TableView自动向上滚动,破坏了流畅的滚动体验。既要避免全量reloadData,又要保留自定义动画,核心是控制插入新数据后的滚动位置,同时做好状态管理防止重复加载。

核心思路

  1. 用一个加载状态标记避免重复触发分页请求;
  2. 插入新数据前记录当前的contentOffsetcontentSize
  3. 插入新行后,手动调整contentOffset来抵消contentSize变化带来的滚动偏移;
  4. 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

火山引擎 最新活动