Search code examples
iosswiftuibezierpathcareplicatorlayer

Round the top and bottom of dashed line drawn with UIBezierPath


I am creating the circle below using a UIBezierPath. Notice that that there are two different colors on opposite sides of the circle. I would like to round the top and bottom of each little rectangle in the picture.

flat tops

I want each one of the rectangles' (they are technically dashes) to have a rounded top and bottom. Like this:

rounded tops

Currently I am using this code to make the circle:

let bounds = self.view.bounds
    let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)

    let whiteLayer = CAShapeLayer()
    let redLayer = CAShapeLayer()

    whiteLayer.fillColor = UIColor.clear.cgColor
    whiteLayer.strokeColor = UIColor.white.cgColor

    var whiteRing = UIBezierPath(arcCenter: arcCenter, radius: CGFloat(125), startAngle: 0, endAngle: CGFloat.pi, clockwise: false)
    whiteRing.rotateAroundPoint(angle: CGFloat.pi/2, center: arcCenter)
    whiteLayer.lineWidth = 10
    whiteLayer.lineDashPattern = [2, 3]
    whiteLayer.path = whiteRing.cgPath

    redLayer.fillColor = UIColor.clear.cgColor
    redLayer.strokeColor = UIColor.red.cgColor
    var redRing = UIBezierPath(arcCenter: arcCenter, radius: CGFloat(125), startAngle: 0, endAngle: CGFloat.pi, clockwise: true)
    redRing.rotateAroundPoint(angle: CGFloat.pi/2, center: arcCenter)
    redLayer.lineWidth = 10
    redLayer.lineDashPattern = [2, 3]
    redLayer.path = redRing.cgPath
    
    self.view.layer.addSublayer(whiteLayer)
   self.view.layer.addSublayer(redLayer)

I honestly don't know if this is possible with a UIBezierPath, I was trying to use something like a CAReplicatorLayer to accomplish this, but I can't seem to figure out how to consistently get the same kind of spacing in between each dash/rectangle and the two separate parts of the circle to avoid overlapping of the white and red parts. Doing this without having to have two different colors would be much easier, but for my use case I must have two different colors. So is there anyway to do this with UIBezierPath, and if not, how would I use CAReplicatorLayer to create the circle in the first picture with each little dash/rectangle having a rounded top and bottom?


Solution

  • Rather than dashing a circular path, stroke a series of line segments going out from the center of the circle:

    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    let path = UIBezierPath()
    let maxRadius = (min(bounds.width, bounds.height) - lineWidth) / 2
    let minRadius = maxRadius - tickLength + lineWidth
    
    for i in 0 ..< tickCount {
        let angle = startAngle + (endAngle - startAngle) * CGFloat(i) / CGFloat(tickCount)
        let startPoint = center.point(at: angle, distance: minRadius)
        let endPoint = center.point(at: angle, distance: maxRadius)
        path.move(to: startPoint)
        path.addLine(to: endPoint)
    }
    

    That uses this method for calculating the start and end points of all of those line segments:

    private extension CGPoint {
        func point(at angle: CGFloat, distance: CGFloat) -> CGPoint {
            return CGPoint(x: x + distance * cos(angle), y: y + distance * sin(angle))
        }
    }
    

    Anyway, you can then apply a line width and corner rounding when you create your shape layer:

    let shapeLayer = CAShapeLayer()
    shapeLayer.lineCap = .round
    shapeLayer.lineWidth = lineWidth
    shapeLayer.fillColor = UIColor.clear.cgColor
    shapeLayer.strokeColor = ...
    shapeLayer.path = path.cgPath
    

    Now, the problem with this is if you want to animate the changes in the red/white strokes. If you need to do that, another approach is to consider making the tick marks a mask, and animate the red stroke underneath it. E.g.

    • Create a view with a white background.
    • Create a bezier path for all of the tick marks.
    • Use that in a shape layer
    • Set the view’s layer’s mask to be that shape layer.
    • Now add a CAShapeLayer for the red path, and animate that:

    enter image description here

    That way, you get corner rounding on your tick marks, but you can still perform animations. And it eliminates any issues associated with getting the tick marks to line up, because you have a single path for all the tick marks.