How to make an animated Circle Progress View

Juan Navas
4 min readFeb 14, 2021

CAShapeLayer and UIBezierPath are going to be your best buddies to draw a circle.

You can combine them like we do in the following snippet in order to get a arc drawn, if you add the created layer to a UIView:

Reference cicle for UIBezierPath - Clockwise: true - Clockwise: false

If you initialize a UIBezierPath with a startAngle of 0 radians, the starting point will be as in the figure 1, this would be like the East cardinal point in a circle.

And the arc drawn would be like the one in the middle, with it starting point in 0 radians (East) and end point in Pi/2 radians (South). If you set clockwise to false, then the arc would be drawn counterclockwise, just like in the right image.

So if you want like me to start in the North position, you may want to use a CGFloat extension such as the following:

Now instead, of using 0 radians and Pi/2 as start and end points, you can use the extension to calculate the endAngle, base on the progress of the circle that you want to fill.

We could add another layer, with another bezier path, and a progress of 1, to draw a full gray circle, for a better looking progress circle. Furthermore, using layer.lineCap = .round the start and end points of the arc would have a nice rounded caps for the drawn arc.

So now that we know how to draw a circle, let’s add an animation to the progress layer.

let animation = CABasicAnimation(keyPath: "strokeEnd")

With this keyPath we’re telling the animation the layer’s property we will be animating.

animation.beginTime = CACurrentMediaTime() + delay    animation.duration = duration

beginTime allows to set a firing date for the animation, so you can add a delay, while duration sets the duration in seconds for the animation.

animation.fromValue = 0    
animation.toValue = 1

With these two properties we’re telling from which towhich value we want thestrokeEnd property to be animated. Since we told the arc we would have a full angle of 90º (Pi/2 radians), 0 would be 0º and 1 would be 90º. In other words, fromValue and toValue are expressed as a percentage of the value to be animated.

animation.timingFunction = CAMediaTimingFunction(name: .easeIn)    animation.fillMode = .forwards    
animation.isRemovedOnCompletion = false

With timingFunction we can specify the animation curve, while fillMode determines how the timed object behaves once its active duration has completed. With .fillMode = .forwards the receiver remains visible in its final state when the animation is completed. That combined with .isRemovedOnCompletion allows that the orange arc remains visible at the end of the animation.

There are other ways to make the object remain visible after the animation, but this one serves our needs and it’s easy enough. But you could use var delegate: CAAnimationDelegate? and conform to that delegate to use the method: optional func animationDidStop(_ anim: CAAnimation, finished flag: Bool) to set the strokeEndto its final value there.

layer.removeAnimation(forKey: "circleAnimation")
layer.strokeEnd = 0
layer.add(animation, forKey: "circleAnimation")

Finally we add the animation using the key circleAnimation, this is just how we want to name our animation. It can be set to nil and it will continue to work, with one caveat; if in this same layer we wanted to play the animation again, if the key was set to nil the new animation wouldn’t be added. That’s why we are making sure the strokeEnd is set to its initial value, and we remove any previous animation with that same key so that it can be added again, and it will just animate beautifully as many times as needed.

If we don’t remove the animations for the specified key, changes to that property will be ignored because the animation is controlling that property. That’s why we are removing it prior to setting the strokeEnd back again to its initial value.

I hope you find this article useful and you’ve learned something as I did along the way 😊.

You can find the full code (for a playground) here.

--

--

Juan Navas

iOS software engineer @ Qonto. Proud dad of 2 little girls. Avid show consumer. Real Madrid fan.