Half-Circle Progress Indicator on Swift. How to draw a half-circle with UIBezierPath

I want to share my experience in custom progress indicator creation. I will show how to create a custom progress indicator drawn with UIBezierPath like bellow:

So let’s start with the idea. We need to create 3 layers (CALayer) for the first indicator, and 2 layers for the second indicator. The main trick that 3 half-circles just clip each other like bellow:

This is a full class for Indicator. It has 2 modes – with the inner circle and without it, like on a first gif.
And now I will explain how it works.
Let’s discuss getOutherGrayCircle function. In this code, we have 3 functions for each half-circle. As they all are half-circles, then we need to understand how to draw only the first half-circle and 2 others will be similarly drawn.
func getOutherGrayCircle() -> CAShapeLayer {
let center = CGPoint(x: fullSize.width / 2, y: fullSize.height)
let beizerPath = UIBezierPath()
beizerPath.move(to: center)
beizerPath.addArc(withCenter: center,
radius: grayCircleSize.width / 2,
startAngle: .pi,
endAngle: 2 * .pi,
clockwise: true)
beizerPath.close()
let innerGrayCircle = CAShapeLayer()
innerGrayCircle.path = beizerPath.cgPath
innerGrayCircle.fillColor = UIColor.gray.cgColor
return innerGrayCircle
}
The center point of UIBezierPath is a center point by X and max Y on the view (center of the coordinate system on the image below). startAngle is always .pi and endAngle we need to calculate ( it depend on how many percents the indicator should show). endAngle has a limit in 2 * .pi

So right now we can create one half-circle. Two grays half-circles will be static, and that’s why startAngle and endAngle in getOutherGrayCircle and getInnerGrayCircle equals to .pi and 2 * .pi.
We have only 1 dynamic half-circle – getGreenCircle. Here we need to set endAngle to depend on how many percents indicator should show. For example: If we need 60% on the indicator, then:
endAngle = .pi + .pi * 0.6
Now we can create all 3 circles and add them to the parent layer
func drawShape(bounds: CGRect) {
fullSize = bounds.size
grayCircleSize = fullSize
greenCircleSize = CGSize(width: bounds.width - 6.0, height: bounds.width - 6.0)
innerGrayCircleSize = CGSize(width: greenCircleSize.width - 44.0,
height: greenCircleSize.width - 44.0)
let outerCicrcle = getOutherGrayCircle()
let greenCircle = getGreenCircle()
progressLayer = greenCircle
self.layer.addSublayer(outerCicrcle)
self.layer.addSublayer(greenCircle)
if isInnerCircleExist {
let innerGrayCircle = getInnerGrayCircle()
self.layer.addSublayer(innerGrayCircle)
}
self.layer.masksToBounds = true
}
For indicator without inner gray half-circle, we just don’t need to draw this layer. And that’s it.
Resources:
Quickstart with CALayer and CABasicAnimation
How to solve the masksToBounds problem with the shadow of UIView?