img

Software Development Blog

Back

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?

Recent Posts

What are UUID identifiers in Swift?

READ MORE

How to use Comparable protocol

READ MORE