如何缓存UIViewController或UIView动画状态?实现续播中断动画
解决ViewController动画断点续播的方案
嘿,这个问题我之前帮朋友处理过,核心就是要抓住「动画进度的保存与恢复」这个关键点——默认情况下,当你从vcA跳去vcB再回来时,vcA的视图可能被移出层级甚至销毁,系统会自动清掉它的动画,所以咱们得手动把进度存下来,回来的时候接着跑。
下面分两种最常见的动画类型给你讲具体实现,都是实战过的靠谱方案:
一、如果你用的是UIView封装的animate(withDuration:...)动画
UIView的动画其实是基于Core Animation做的上层封装,但它的「重复+自动反转」是逻辑层面的控制,所以咱们得自己跟踪进度,还要计算断点对应的视图状态。
具体步骤:
先定义变量存动画状态
在vcA里加这些属性,用来记录动画的关键信息:// 你的动画总时长,根据实际情况修改 private let totalAnimDuration: TimeInterval = 5.0 // 记录当前动画跑了多少进度(0.0到1.0,超过1对应重复的周期) private var animProgress: CGFloat = 0.0 // 标记动画是否正在运行 private var isAnimating = false // 用CADisplayLink精准跟踪进度,比普通定时器更贴合屏幕刷新 private var displayLink: CADisplayLink?写通用的启动/恢复动画方法
别直接在viewDidLoad里硬写动画,搞个专门的方法,既能首次启动,也能从断点恢复:private func startAnim(from progress: CGFloat = 0.0) { // 先停掉之前的动画和进度跟踪器 stopAnim() // 先把视图设置到断点对应的状态 yourAnimatedView.transform = getTransform(for: progress) isAnimating = true animProgress = progress // 计算剩余需要运行的时长 let remainingDuration = totalAnimDuration * TimeInterval(1.0 - progress.truncatingRemainder(dividingBy: 1.0)) // 启动剩余动画,保持原有的repeat和autoreverse配置 UIView.animate(withDuration: remainingDuration, delay: 0, options: [.repeat, .autoreverse], animations: { // 这里写你原本的动画逻辑,比如让视图右移200pt self.yourAnimatedView.transform = CGAffineTransform(translationX: 200, y: 0) }) { [weak self] _ in self?.isAnimating = false } // 启动进度跟踪,实时更新animProgress displayLink = CADisplayLink(target: self, selector: #selector(updateProgress)) displayLink?.add(to: .main, forMode: .common) } // 根据进度计算当前视图该有的transform(适配自动反转的逻辑) private func getTransform(for progress: CGFloat) -> CGAffineTransform { // 因为是自动反转,一个完整周期(去+回)是2倍总时长,所以进度取余1.0后分两段处理 let cycleProgress = progress.truncatingRemainder(dividingBy: 1.0) if cycleProgress <= 0.5 { // 正向阶段:从0到200pt,进度是cycleProgress*2 let translation = 200 * cycleProgress * 2 return CGAffineTransform(translationX: translation, y: 0) } else { // 反向阶段:从200回到0,进度是(1 - (cycleProgress - 0.5)*2) let translation = 200 * (1 - (cycleProgress - 0.5)*2) return CGAffineTransform(translationX: translation, y: 0) } } @objc private func updateProgress() { guard isAnimating else { return } let elapsed = displayLink?.duration ?? 0 animProgress += CGFloat(elapsed / totalAnimDuration) }在VC生命周期中处理暂停与恢复
当vcA要消失时,暂停动画并保存进度;回来时,从断点继续运行:override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) stopAnim() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if animProgress > 0 { // 从断点恢复动画 startAnim(from: animProgress) } else { // 首次启动动画 startAnim() } } private func stopAnim() { isAnimating = false yourAnimatedView.layer.removeAllAnimations() displayLink?.invalidate() displayLink = nil }
二、如果你用的是Core Animation(比如CABasicAnimation)
这种情况更省心,因为Core Animation本身就支持直接暂停和恢复动画,操作layer的时间属性就能实现断点续播。
具体步骤:
定义变量存暂停时间
在vcA里加这两个属性:// 记录动画暂停时的时间点 private var pausedTime: CFTimeInterval = 0.0 // 标记动画是否处于暂停状态 private var isAnimPaused = false封装暂停和恢复的方法
这是Core Animation控制进度的标准操作:private func pauseAnim() { guard let layer = yourAnimatedView.layer else { return } // 获取当前媒体时间,转换为layer的本地时间 let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil) // 把layer速度设为0,相当于暂停动画 layer.speed = 0.0 // 设置timeOffset保存当前进度 layer.timeOffset = pausedTime self.pausedTime = pausedTime isAnimPaused = true } private func resumeAnim() { guard let layer = yourAnimatedView.layer else { return } let pausedTime = self.pausedTime // 恢复layer的速度 layer.speed = 1.0 // 重置timeOffset和beginTime,重新计算启动时间 layer.timeOffset = 0.0 layer.beginTime = 0.0 let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime layer.beginTime = timeSincePause isAnimPaused = false }在VC生命周期中调用
同样,消失时暂停,回来时恢复:override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if !isAnimPaused { pauseAnim() } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if isAnimPaused { resumeAnim() } else { // 首次启动Core Animation startCoreAnim() } } // 初始化你的Core Animation private func startCoreAnim() { let anim = CABasicAnimation(keyPath: "transform.translation.x") anim.fromValue = 0 anim.toValue = 200 anim.duration = 5.0 anim.repeatCount = .infinity anim.autoreverses = true yourAnimatedView.layer.add(anim, forKey: "translationAnim") }
几个要注意的坑
- 如果vcA的view被系统销毁了(比如内存不足时),
viewDidLoad会重新执行,这时候你得把进度存在不会被销毁的地方——比如用ViewModel、单例,或者临时存在UserDefaults里(仅适用于临时保存场景)。 - 用UIView动画时,一定要注意自动反转的进度计算,不然恢复的时候可能会跳帧或者方向错误。
- 测试的时候要覆盖push/pop、present/dismiss两种跳转场景,确保每种情况都能正确续播。
内容的提问来源于stack exchange,提问作者Lowji
相关产品推荐
相关产品推荐

