Search code examples
iosswiftuibezierpathswift5cashapelayer

How to fill only a part of the shape on CAShapeLayer


I am creating following shape with UIBezierPath and then draw it with CAShapeLayer as following.

enter image description here

Then I can change CAShapeLayer fillColor property from shapeLayer.fillColor = UIColor.clear.cgColor to shapeLayer.fillColor = UIColor.blue.cgColor and get following shape.

enter image description here

Here is the code:

import UIKit

class ViewController: UIViewController {

    var line = [CGPoint]()
    let bezierPath: UIBezierPath = UIBezierPath()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        let point1 = CGPoint(x: 100, y: 100)
        let point2 = CGPoint(x: 400, y: 100)

        line.append(point1)
        line.append(point2)

        addCircle(toRight: false)
        addLine()
        addCircle(toRight: true)

        let shapeLayer = CAShapeLayer()
        shapeLayer.path = bezierPath.cgPath
        shapeLayer.strokeColor = UIColor.blue.cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor

         self.view.layer.addSublayer(shapeLayer)
    }

    func addLine() -> Void {
        bezierPath.move(to: line.first!)
        bezierPath.addLine(to: line.last!)
    }

    func addCircle(toRight: Bool) -> Void {
        let angle = CGFloat( Double.pi / 180 )
        let r = CGFloat(20.0)
        let x0 = toRight ? line.first!.x : line.last!.x
        let y0 = toRight ? line.first!.y : line.last!.y
        let x1 =  toRight ? line.last!.x : line.first!.x
        let y1 = toRight ? line.last!.y : line.first!.y
        let x = x1 - x0
        let y = y1 - y0
        let h = (x*x + y*y).squareRoot()
        let x2 = x0 + (x * (h + r) / h)
        let y2 = y0 + (y * (h + r) / h)

        // Add the arc, starting at that same point
        let point2 = CGPoint(x: x2, y: y2)
        let pointZeroDeg = CGPoint(x: x2 + r, y: y2)
        self.bezierPath.move(to: pointZeroDeg)
        self.bezierPath.addArc(withCenter: point2, radius: r,
                          startAngle: 0*angle, endAngle: 360*angle,
                          clockwise: true)
        self.bezierPath.close()
    }
}

But what I actually want is following shape (left side circle is filled, right side circle is not filled).

enter image description here

So my question is, How to fill only a part of the shape on CAShapeLayer? Is it possible? Is there any trick to achieve this?

PS: I can achieve this by creating 3 different UIBezierPaths (for leftCircle, line and rightCircle) and drawing them with 3 different CAShapeLayers as following.

// left Circle
shapeLayer1.path = bezierPath1.cgPath
shapeLayer1.fillColor = UIColor.blue.cgColor

// line
shapeLayer2.path = bezierPath2.cgPath

// right Circle
shapeLayer3.path = bezierPath3.cgPath
shapeLayer3.fillColor = UIColor.clear.cgColor

self.view.layer.addSublayer(shapeLayer1)
self.view.layer.addSublayer(shapeLayer2)
self.view.layer.addSublayer(shapeLayer3)

But I prefer to achieve this with a single CAShapeLayer.


Solution

  • I think better approach is using of multiple shapes.

    However you can make one-shape implementation by using evenOdd rule of shape filling.

    Just add an extra circle to the right circle in method addCircle:

    if toRight {
        self.bezierPath.addArc(withCenter: point2, radius: r,
                          startAngle: 0*angle, endAngle: 2*360*angle,
                          clockwise: true)
    } else {
        self.bezierPath.addArc(withCenter: point2, radius: r,
                          startAngle: 0*angle, endAngle: 360*angle,
                          clockwise: true)
    }
    

    And set fillRule to evenOdd:

    shapeLayer.fillColor = UIColor.blue.cgColor        
    shapeLayer.fillRule = .evenOdd
    

    So you'll get that the left circle will have one circle of drawing and will be filled by evenOdd rule. But right circle will have two circles of drawing and will be unfilled.

    The evenOdd rule (https://developer.apple.com/documentation/quartzcore/cashapelayerfillrule/1521843-evenodd):

    Specifies the even-odd winding rule. Count the total number of path crossings. If the number of crossings is even, the point is outside the path. If the number of crossings is odd, the point is inside the path and the region containing it should be filled.