You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何缓存UIViewController或UIView动画状态?实现续播中断动画

解决ViewController动画断点续播的方案

嘿,这个问题我之前帮朋友处理过,核心就是要抓住「动画进度的保存与恢复」这个关键点——默认情况下,当你从vcA跳去vcB再回来时,vcA的视图可能被移出层级甚至销毁,系统会自动清掉它的动画,所以咱们得手动把进度存下来,回来的时候接着跑。

下面分两种最常见的动画类型给你讲具体实现,都是实战过的靠谱方案:


一、如果你用的是UIView封装的animate(withDuration:...)动画

UIView的动画其实是基于Core Animation做的上层封装,但它的「重复+自动反转」是逻辑层面的控制,所以咱们得自己跟踪进度,还要计算断点对应的视图状态。

具体步骤:

  1. 先定义变量存动画状态
    在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?
    
  2. 写通用的启动/恢复动画方法
    别直接在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)
    }
    
  3. 在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的时间属性就能实现断点续播。

具体步骤:

  1. 定义变量存暂停时间
    在vcA里加这两个属性:

    // 记录动画暂停时的时间点
    private var pausedTime: CFTimeInterval = 0.0
    // 标记动画是否处于暂停状态
    private var isAnimPaused = false
    
  2. 封装暂停和恢复的方法
    这是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
    }
    
  3. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 06:37:35