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

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:

Github project

Quickstart with CALayer and CABasicAnimation

How to solve the masksToBounds problem with the shadow of UIView?