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

CABasicAnimation完成速度远快于Timer的问题求助

CABasicAnimation进度条与Timer不同步的问题排查与修复

嘿,我仔细看了你的代码,终于搞清楚为什么进度条总是提前填满了,而且时长越长差异越大——核心是路径角度设置错了,还有动画逻辑和倒计时反了,另外还有个布局时机的小坑,咱们一步步来解决:

问题根源

  1. 圆形路径的角度范围不对
    你创建的UIBezierPath用了startAngle: -CGFloat.pi/2和endAngle: 2 * CGFloat.pi,这意味着路径的总角度是2π - (-π/2) = 5π/2(也就是450度,相当于1.25个完整的圆)。而strokeEnd从0到1对应的是整个路径的长度,所以当动画执行到总时长的80%时(360/450=0.8),进度条就已经画完一圈了,看起来填满了,但Timer还剩20%的时间没走完。比如你设30秒的话,进度条24秒就满了,Timer还要再走6秒,时长越长这个差值就越大。

  2. 动画逻辑和倒计时反了
    你的Timer是倒计时(从设定时长减到0),但动画却是从空(strokeEnd=0)填充到满(strokeEnd=1),这不仅不符合用户对倒计时进度条的直觉,也加深了视觉上的“不同步”错觉。

  3. 布局时机错误
    在viewDidLoad里用timerContainer.center创建路径,这时候AutoLayout还没完成,timerContainer的实际位置和你拿到的center可能不一致,导致路径位置偏移,看起来也会怪怪的。

修复步骤

1. 修正圆形路径的角度

把路径的endAngle改成3 * CGFloat.pi/2,这样从-π/2(底部)顺时针到3π/2(底部),总角度正好是2π(360度),对应完整的一个圆:

let circularPath = UIBezierPath(arcCenter: timerContainer.center, radius: 35, startAngle: -CGFloat.pi / 2, endAngle: 3 * CGFloat.pi / 2, clockwise: true)

2. 调整动画方向匹配倒计时

  • 初始时把shapeLayer.strokeEnd设为1(进度条满格)
  • 动画的toValue改成0,让进度条随着时间逐渐清空:
// 在viewDidLayoutSubviews里修改(后续会移到这里)
shapeLayer.strokeEnd = 1

// 在start()方法里修改动画
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 0  // 从满到空,匹配倒计时逻辑
basicAnimation.fillMode = .forwards
basicAnimation.duration = CFTimeInterval(minutes * 60 + seconds)
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "begin")

3. 修复路径位置偏移问题

把创建路径和添加图层的代码从viewDidLoad移到viewDidLayoutSubviews,这个方法在AutoLayout完成布局后调用,能保证timerContainer.center是正确的位置。另外要把trackLayer改成类属性,避免重复添加:

// 类属性添加trackLayer
let shapeLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    // 先移除旧图层,避免重复添加
    trackLayer.removeFromSuperlayer()
    shapeLayer.removeFromSuperlayer()
    
    let circularPath = UIBezierPath(arcCenter: timerContainer.center, radius: 35, startAngle: -CGFloat.pi / 2, endAngle: 3 * CGFloat.pi / 2, clockwise: true)
    
    trackLayer.path = circularPath.cgPath
    trackLayer.strokeColor = UIColor.lightGray.cgColor
    trackLayer.lineWidth = 5
    trackLayer.fillColor = UIColor.clear.cgColor
    
    shapeLayer.path = circularPath.cgPath
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.lineWidth = 5
    shapeLayer.fillColor = UIColor.clear.cgColor
    shapeLayer.strokeEnd = 1
    shapeLayer.lineCap = .round
    
    view.layer.addSublayer(trackLayer)
    view.layer.addSublayer(shapeLayer)
}

4. 可选:优化Timer精度(解决延迟问题)

Timer的1秒触发间隔不是绝对精确的,当有UI交互时可能会延迟。如果需要更高精度的同步,可以用CADisplayLink结合时间戳来计算剩余时间:

// 替换原有的Timer相关代码
var startTime: Date?
var displayLink: CADisplayLink?

@objc func updateTimer() {
    guard let startTime = startTime else { return }
    let elapsed = Date().timeIntervalSince(startTime)
    let totalTime = Double(minutes * 60 + seconds)
    let remaining = totalTime - elapsed
    
    if remaining <= 0 {
        displayLink?.invalidate()
        minutes = 0
        seconds = 0
    } else {
        minutes = Int(remaining / 60)
        seconds = Int(remaining.truncatingRemainder(dividingBy: 60))
    }
    displayTimer()
}

@IBAction func start() {
    startTime = Date()
    displayLink = CADisplayLink(target: self, selector: #selector(updateTimer))
    displayLink?.add(to: .main, forMode: .common)
    
    // 动画代码不变
    let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
    basicAnimation.toValue = 0
    basicAnimation.fillMode = .forwards
    basicAnimation.duration = CFTimeInterval(minutes * 60 + seconds)
    basicAnimation.isRemovedOnCompletion = false
    shapeLayer.add(basicAnimation, forKey: "begin")
}

效果验证

修改完成后,进度条会从满格开始,随着倒计时逐渐清空,动画时长和倒计时总时长完全匹配,不会再出现提前填满的情况,路径位置也会精准对应timerContainer的中心。

内容的提问来源于stack exchange,提问作者Andy_V

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 12:42:55