iosswiftuikitcore-animation

Rounded corner of a cut-out arc using UIBezierPath


I'm drawing a cut-out hole in CAShapeLayer using UIBezierPath, this is the code I have so far:

let view = UIView(
  frame: CGRect(origin: .zero, size: CGSize(width: 500.0, height: 500.0))
)
view.backgroundColor = .clear

let shapeLayer = CAShapeLayer()
shapeLayer.frame = view.bounds
shapeLayer.fillColor = UIColor.systemMint.cgColor

let mainPath = UIBezierPath(
  roundedRect: CGRect(
    origin: CGPoint(x: 50.0, y: 50.0), size: CGSize(width: 400.0, height: 400.0)
  ),
  cornerRadius: 10.0
)

let cutOutArc = UIBezierPath()
cutOutArc.addArc(
  withCenter: CGPoint(x: 50.0, y: 250.0),
  radius: 50.0,
  startAngle: .pi / 2.0,
  endAngle: .pi + .pi / 2.0,
  clockwise: false
)
cutOutArc.addLine(to: CGPoint(x: 50.0, y: 200.0))
cutOutArc.close()

mainPath.append(cutOutArc)

shapeLayer.fillRule = .evenOdd
shapeLayer.path = mainPath.cgPath

view.layer.insertSublayer(shapeLayer, at: 0)

(The code is simplified and adapted to use in Playground.)

The code results in this shape (looks exactly as I need):

A square shape with a half-arc cut-out hole in the centre of the left edge

What I can't figure out how to do is adding a corner radius to the cut-out arc (where the red arrows in the image below point), say, of 4.0 points. I've played with pretty random options for a couple days now with no meaningful result. I tried to ask AI, and it couldn't figure out it either (it just kept on producing broken code really).

A square shape with a half-arc cut-out hole in the centre of the left edge, and red arrows pointing to the place, where I need to add corner radius

Does anyone know how to do it? Any guidance is much appreciated!


Solution

  • You can move the center of the arc a little bit to the right, by the corner radius that you want, so that the rounded corners "join up" with the semicircular arc.

    That is, you are connecting up two little rounded 90-degree arcs with a big 180-degree arc. This picture shows the two little arcs,

    enter image description here

    and after having the 180-degree arc sit on top of them, it becomes

    enter image description here

    The little arcs (including the ones for the rounded rectangle) can be drawn by spamming addArc(tangent1End:tangent2End:radius:), using a similar technique as I described here.

    The tangent end points look like this:

    enter image description here

    In the code I have marked the corresponding points (1-7) in comments,

    func createPath(
        width: CGFloat,
        height: CGFloat,
        cutoutRadius: CGFloat,
        cornerRadius: CGFloat
    ) -> CGPath {
        let center = CGPoint(x: cornerRadius, y: height / 2)
        let path = CGMutablePath()
        path.move(to: .init(x: 0, y: height - cornerRadius)) // 1
        path.addArc(
            tangent1End: .init(x: 0, y: height / 2 + cutoutRadius), // 2
            tangent2End: .init(x: cutoutRadius, y: height / 2 + cutoutRadius), // 3
            radius: cornerRadius
        )
        
        path.addArc(
            center: center,
            radius: cutoutRadius,
            startAngle: .pi / 2,
            endAngle: 3 * .pi / 2,
            clockwise: true
        )
        // now path.currentPoint is at X
        
        path.addArc(
            tangent1End: .init(x: 0, y: height / 2 - cutoutRadius), // 4
            tangent2End: .zero, // 5
            radius: cornerRadius
        )
        path.addArc(
            tangent1End: .zero, // 5
            tangent2End: .init(x: width, y: 0), // 6
            radius: cornerRadius
        )
        path.addArc(
            tangent1End: .init(x: width, y: 0), // 6
            tangent2End: .init(x: width, y: height), // 7
            radius: cornerRadius
        )
        path.addArc(
            tangent1End: .init(x: width, y: height), // 7
            tangent2End: .init(x: 0, y: height), // 1
            radius: cornerRadius
        )
        path.addArc(
            tangent1End: .init(x: 0, y: height), // 1
            tangent2End: .zero,
            radius: cornerRadius
        )
        path.closeSubpath()
    
        return path
    }