Core Animation - Press and Hold


Recently I was trying to create a Press and hold animation that was controlled by the user tapping, holding and releasing a button.

  • While the button is tapped, the animation runs and the circle fills with another color.
  • When the button is released, the circle empties again, reverting to its previous color.
  • The filling should be interactive, allowing the user to play with the animation by tapping and releasing the button randomly.

Sounds simple enough, right? Just start a simple animation and reverse it when necessary.

First attempts

UIView animations sadly don’t work for this. They only work on UIView properties and they can’t be interrupted.

After searching some more, I came across David Rnnqvist’s excellent Controlling Animation Timing. Read that, it will make you understand CAAnimation so much better!

That gave me idea to set the layer’s speed and timeOffset of the layer to 0 and to control the animation by changing the timeOffset with the button press. However, this only works if you continuously trigger a signal, for example by moving your finger. But we want to press and hold our finger in place!

Another attempt was to set the layer.speed = 0, set it to 1 on a press and to -1 when you remove your finger. That doesn’t work either! 😣

Final solution

In the end, I went with the following approach. On the press, we create an animation. When we lift our finger, we do the following;

  1. Create a new reverse animation.
  2. Pause the first animation (as mentioned in this Apple example.)
  3. Do some calculations to restart the reverse animation at the correct point.
  4. Remove the first animation, and add the reverse one to the layer.
  5. Fun!

Here’s my final method;

func setAnimation(layer: CAShapeLayer, startPath: AnyObject, endPath: AnyObject, duration: Double)
{
    // Always create a new animation.
    let animation: CABasicAnimation = CABasicAnimation(keyPath: "path")

    if let currentAnimation = layer.animationForKey("animation") as? CABasicAnimation {
        // If an animation exists, reverse it.
        animation.fromValue = currentAnimation.toValue
        animation.toValue = currentAnimation.fromValue

        let pauseTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
        // For the timeSinceStart, we take the minimum from the duration or the time passed.
        // If not, holding the animation longer than its duration would cause a delay in the reverse animation.
        let timeSinceStart = min(pauseTime - startTime, currentAnimation.duration)

        // Now convert for the reverse animation.
        let reversePauseTime = currentAnimation.duration - timeSinceStart
        animation.beginTime = pauseTime - reversePauseTime

        // Remove the old animation
        layer.removeAnimationForKey("animation")
        // Reset startTime, to be when the reverse WOULD HAVE started.
        startTime = animation.beginTime
    }
    else {
        // This happens when there is no current animation.
        startTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)

        animation.fromValue = startPath
        animation.toValue = endPath
    }

    animation.duration = duration
    animation.fillMode = kCAFillModeForwards // These lines are important to keep the animation at its final frame.
    animation.removedOnCompletion = false    // If not, the animation would remove itself before showing a reverse.

    layer.addAnimation(animation, forKey: "animation")
}

You can find my final result on Github. If you found this useful, let me know on Twitter :)