Search code examples
swiftcore-animationcashapelayer

Change Color of an Animated UIBezierPath at a Certain Point


I am trying to create a circular progress bar in Swift 4 using a CAShapeLayer and animated UIBezierPath. This works fine but I would like the circle to change it's strokeColor once the animation reaches a certain value.

For example: Once the circle is 75% drawn I want to switch the strokeColor from UIColor.black.cgColor to UIColor.red.cgColor.

My code for the circle and the "progress" animation looks like this:

let circleLayer = CAShapeLayer()

// set initial strokeColor:
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.path = UIBezierPath([...]).cgPath

// animate the circle:
let animation = CABasicAnimation()
animation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
animation.fromValue = 0.0
animation.toValue = 1
animation.duration = 10
animation.isAdditive = true
animation.fillMode = .forwards
circleLayer.add(animation, forKey: "strokeEnd")

I know that is also possible to create a CABasicAnimation for the strokeColor keypath and set the fromValue and toValue to UIColors to get the strokeColor to slowly change. But this is like a transition over time which is not exactly what I want.

Update 1:

Based on Mihai Fratu's answer I was able to solve my problem. For future reference I want to add a minimal Swift 4 code example:

// Create the layer with the circle path (UIBezierPath)
let circlePathLayer = CAShapeLayer()
circlePathLayer.path = UIBezierPath([...]).cgPath
circlePathLayer.strokeEnd = 0.0
circlePathLayer.strokeColor = UIColor.black.cgColor
circlePathLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(circlePathLayer)

// Create animation to animate the progress (circle slowly draws)
let progressAnimation = CABasicAnimation()
progressAnimation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
progressAnimation.fromValue = 0.0
progressAnimation.toValue = 1

// Create animation to change the color
let colorAnimation = CABasicAnimation()
colorAnimation.keyPath = #keyPath(CAShapeLayer.strokeColor)
colorAnimation.fromValue = UIColor.black.cgColor
colorAnimation.toValue = UIColor.red.cgColor
colorAnimation.beginTime = 3.75 // Since your total animation is 10s long, 75% is 7.5s - play with this if you need something else
colorAnimation.duration = 0.001 // make this really small - this way you "hide" the transition
colorAnimation.fillMode = .forwards

// Group animations together
let progressAndColorAnimation = CAAnimationGroup()
progressAndColorAnimation.animations = [progressAnimation, colorAnimation]
progressAndColorAnimation.duration = 5

// Add animations to the layer
circlePathLayer.add(progressAndColorAnimation, forKey: "strokeEndAndColor")

Solution

  • If I understood your question right this should do what you are after. Please bare in mind that it's not tested at all:

    let circleLayer = CAShapeLayer()
    
    // set initial strokeColor:
    circleLayer.strokeColor = UIColor.black.cgColor
    circleLayer.path = UIBezierPath([...]).cgPath
    
    // animate the circle:
    let animation = CABasicAnimation()
    animation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
    animation.fromValue = 0.0
    animation.toValue = 1
    animation.beginTime = 0 // Being part of an animation group this is relative to the animation group start time
    animation.duration = 10
    animation.isAdditive = true
    animation.fillMode = .forwards
    
    // animate the circle color:
    let colorAnimation = CABasicAnimation()
    colorAnimation.keyPath = #keyPath(CAShapeLayer.strokeColor)
    colorAnimation.fromValue = UIColor.black.cgColor
    colorAnimation.toValue = UIColor.black.red
    colorAnimation.beginTime = 7.5 // Since your total animation is 10s long, 75% is 7.5s - play with this if you need something else
    colorAnimation.duration = 0.0001 // make this really small - this way you "hide" the transition
    colorAnimation.isAdditive = true
    colorAnimation.fillMode = .forwards
    
    let sizeAndColorAnimation = CAAnimationGroup()
    sizeAndColorAnimation.animations = [animation, colorAnimation]
    sizeAndColorAnimation.duration = 10
    
    circleLayer.add(sizeAndColorAnimation, forKey: "strokeEndAndColor")