Search code examples
iosswiftuikit

Circular progress view with outside stroke


I am trying to create a circular progress view that has stroke outside. But my stroke starts from inside of the view instead of the start from the outside it like border. How can I solve it?

My code:

var progressLayer = CAShapeLayer()
var trackLayer = CAShapeLayer()

var progressLineWidth: CGFloat = 20
var progressColor = UIColor.green
var progress: Float = 0.5

let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0 - 2, y: frame.size.height / 2.0 - 2), radius: (frame.size.width) / 2, startAngle: -(.pi / 2), endAngle: .pi * 1.5, clockwise: true)

trackLayer.path = circlePath.cgPath
trackLayer.strokeColor = UIColor.gray.cgColor
trackLayer.lineWidth = 10
trackLayer.strokeEnd = 1.0
trackLayer.lineDashPattern = [4,4]
trackLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(trackLayer)

progressLayer.path = circlePath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = progressColor.cgColor
progressLayer.lineWidth = progressLineWidth
progressLayer.strokeEnd = CGFloat(progress)
progressLayer.strokeStart = 0
progressLayer.lineDashPattern = [4,4]
layer.addSublayer(progressLayer)

Result:

enter image description here

What I want to achive:

enter image description here

Gray and green strokes should start from the outside of the red circle.


Solution

  • Demo

    You can subtract the middle circle by a simple mask like:

    let maskLayer = CAShapeLayer()
    maskLayer.path = UIBezierPath(rect: self.bounds).cgPath.subtracting(circlePath.cgPath)
    layer.mask = maskLayer
    

    Full working code: - (updated to support iOS below 16)

    class CircularProgressBarView: UIView {
        var progress: CGFloat = 0 { didSet { layoutSubviews() } }
        var progressLineWidth: CGFloat = 20 { didSet { layoutSubviews() } }
        var progressColor = UIColor.green { didSet { layoutSubviews() } }
        var pattern: [NSNumber] = [4,4] { didSet { layoutSubviews() } }
    
        private var customLayer = CALayer()
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            customLayer.removeFromSuperlayer()
            customLayer = CALayer()
    
            let bounds = CGRect(
                x: progressLineWidth,
                y: progressLineWidth,
                width: bounds.width - progressLineWidth*2,
                height: bounds.height - progressLineWidth*2
            )
    
            let circlePath = UIBezierPath(ovalIn: bounds)
    
            let trackLayer = CAShapeLayer()
            trackLayer.path = circlePath.cgPath
            trackLayer.strokeColor = UIColor.gray.cgColor
            trackLayer.lineWidth = progressLineWidth
            trackLayer.lineDashPattern = pattern
            trackLayer.fillColor = UIColor.red.cgColor
            customLayer.addSublayer(trackLayer)
    
            let progressLayer = CAShapeLayer()
            progressLayer.path = circlePath.cgPath
            progressLayer.fillColor = UIColor.clear.cgColor
            progressLayer.strokeColor = progressColor.cgColor
            progressLayer.lineWidth = progressLineWidth * 2
            progressLayer.strokeEnd = progress
            progressLayer.lineDashPattern = pattern
            customLayer.addSublayer(progressLayer)
    
            let containerPath = UIBezierPath(rect: self.bounds)
            containerPath.append(circlePath.reversing())
            let maskLayer = CAShapeLayer()
            maskLayer.path = containerPath.cgPath
            customLayer.mask = maskLayer
    
            layer.addSublayer(customLayer)
        }
    }
    

    Of course you can rotate the whole layer (or view) to make it start from 12 O'clock