iosmacoscore-animation

Using CALayer.speed to finish running animation


Suppose I have a long running task whose progress is displayed in custom progress bar similar to system progress bar. A task can finish faster than animation of CALayer.bounds from leading to trailing edge. In this case I need to fast forward animation to the trailing edge.

Currently I replace running animation with a copy but with a much shorter duration. This approach works fine but requires boilerplate code.

I'm wondering is it possible to achieve the same result but with CALayer.speed only (eg. set it to 2 or 3)? Naive approach to pause and resume animation with higher speed yields to unexpected result: progress layer just disappears.


Solution

  • So, it appears you are running a "progress bar" animation by animating the bounds of a layer.

    Your comments state "3 minutes" so you probably have code like this:

    // create bounds animation for width = 0 to width = bounds.width
    let anim = CABasicAnimation(keyPath: "bounds")
    anim.fromValue = CGRect(x: 0.0, y: 0.0, width: 0.0, height: bounds.height)
    anim.toValue = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
    anim.fillMode = .forwards
    anim.isRemovedOnCompletion = false
    
    // 3-minute duration == 180 seconds
    anim.duration = 180
        
    // start the animation
    progressLayer.add(anim, forKey: "progress")
    

    Then, if the "task" is completed in, say, 2-minutes and 15-seconds, you want to "speed up" the remaining portion of the animation.

    The simplest way - with no need for changing the layer speed - would be:

    progressLayer.removeAllAnimations()
    progressLayer.bounds = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
    

    This will stop the slow animation and immediately extend the layer bounds to full width. Note that it will use CALayer built-in animation, so it will take 0.3-seconds for the layer to reach full width.

    If you want the progress bar to "snap" directly to full width, you can wrap that in a CATransaction block and disable the default 0.3-second animation:

    CATransaction.begin()
    CATransaction.setDisableActions(true)
    progressLayer.removeAllAnimations()
    progressLayer.bounds = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
    CATransaction.commit()
    

    If you actually want the animation to continue, but the default 0.3-seconds is too quick, you can change the layer speed like this:

    progressLayer.timeOffset = progressLayer.convertTime(CACurrentMediaTime(), from: nil)
    progressLayer.beginTime = CACurrentMediaTime()
    
    // changing the layer speed to 2.0 would only *very slightly* increase the speed
    //  start with the same value as the duration and observe the result
    //  adjust this value until you're happy with the "speeded up" animation completion
    progressLayer.speed = 180.0