如何在滚动UITableView时强制停止不可见UITableViewCell的动画
问题根因
- 原方案仅在
cellForRowAt出队时清理动画,cell滚出屏幕时没有及时终止绑定的动画,已经启动的动画闭包仍持有cell弱引用,等动画触发时cell已经被复用给其他indexPath,导致动画错乱 - 动画执行过程中没有校验归属,即使cell被复用,不属于当前indexPath的旧动画仍会执行,影响当前cell的展示
修复方案
按照以下修改即可解决问题:
- 给自定义cell增加绑定行标识,用于校验动画归属
- 重写cell复用前的清理方法,主动清空旧动画和状态
- 实现cell滚出屏幕的代理方法,及时停止动画并标记动画完成
- 所有动画闭包增加归属校验,避免复用后执行旧动画
完整修复代码
import UIKit class TestCell: UITableViewCell { var animations: [UIViewPropertyAnimator] = [] // 存储当前cell绑定的row,用于校验动画归属 var currentBindRow: Int = -1 override func prepareForReuse() { super.prepareForReuse() // 复用时立刻停止所有动画,清空状态 animations.forEach { animator in animator.stopAnimation(true) } animations.removeAll() // 重置视图到初始状态,避免旧内容残留 textLabel?.layer.opacity = 1 textLabel?.text = nil currentBindRow = -1 } } class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! // 标记对应row是否已经完成动画(包含滚出屏幕视为完成的情况) var animated: [Bool] = Array(repeating: false, count: 100) override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.delegate = self tableView.register(TestCell.self, forCellReuseIdentifier: "TestCell") } } extension ViewController: UITableViewDataSource, UITableViewDelegate { func numberOfSections(in tableView: UITableView) -> Int { 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 100 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as! TestCell // 绑定当前cell对应的row cell.currentBindRow = indexPath.row cell.textLabel?.text = "\(indexPath.row)" if !animated[indexPath.row] { // 第一个动画:透明度降到0 let animator = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in guard let cell = cell, cell.currentBindRow == indexPath.row else { return } cell.textLabel?.layer.opacity = 0 } animator.addCompletion { [weak cell] completed in guard let cell = cell, cell.currentBindRow == indexPath.row else { return } cell.textLabel?.layer.opacity = 0 if completed == .end { // 第二个动画:透明度升到1 let animator1 = UIViewPropertyAnimator(duration: 5, curve: .easeIn) { [weak cell] in guard let cell = cell, cell.currentBindRow == indexPath.row else { return } cell.textLabel?.layer.opacity = 1 } animator1.addCompletion { [weak cell] _ in guard let cell = cell, cell.currentBindRow == indexPath.row else { return } cell.textLabel?.layer.opacity = 1 } animator1.startAnimation(afterDelay: 0) cell.animations.append(animator1) } } animator.startAnimation(afterDelay: 0) cell.animations.append(animator) } else { // 已经执行过动画,直接设为最终状态 cell.textLabel?.layer.opacity = 1 } return cell } // cell滚出可视区域时触发 func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let cell = cell as? TestCell else { return } // 停止当前cell所有动画 cell.animations.forEach { animator in animator.stopAnimation(true) } cell.animations.removeAll() // 标记该indexPath已完成动画,下次进入可视区直接用最终状态 animated[indexPath.row] = true } }
关键改动说明
- 新增
currentBindRow属性绑定cell当前对应的row,所有动画执行前都会校验归属,避免作用到复用后的cell - 重写
prepareForReuse方法,cell复用时主动清空所有动画和视图状态,从根源避免旧动画残留 - 实现
didEndDisplaying代理,cell滚出屏幕时立刻停止动画并标记对应row为已完成,符合「滚出即视为动画完成」的需求逻辑 - 所有动画闭包和回调都增加了归属校验,一旦cell被复用,旧的动画会自动终止执行,不会干扰新的内容
内容的提问来源于stack exchange,提问作者Aswath
相关产品推荐
相关产品推荐

