Search code examples
swiftuiviewuibezierpathcashapelayer

How to create a view with a circle shape cutting it at the bottom?


I would like to create a square UIView with a 50px diameter half-circle cutting it at the bottom, like this:

enter image description here

I tried this code:

class CustomView: UIView {
    override func layoutSubviews() {
        super.layoutSubviews()
        
        let radius = 50.0
        let path = UIBezierPath(
            arcCenter: CGPoint(
                x: bounds.midX,
                y: bounds.maxY
            ),
            radius: radius,
            startAngle: .pi,
            endAngle: 0,
            clockwise: true
        )
        path.addLine(
            to: CGPoint(
                x: bounds.minX,
                y: bounds.maxY
            )
        )
        path.close()
        let maskLayer = CAShapeLayer()
        maskLayer.path = path.cgPath
        layer.mask = maskLayer
    }
}

But instead, I am getting this:

enter image description here

How can I get this to work?


Solution

  • Here's a solution that involves overriding draw and using a UIBezierPath to draw the shape.

    The following can be used in an iOS Swift Playground:

    import UIKit
    import PlaygroundSupport
    
    class CustomView: UIView {
        var radius: CGFloat = 25
        var fillColor: UIColor = .blue
    
        override func draw(_ rect: CGRect) {
            let shape = UIBezierPath()
            shape.move(to: .zero) // top-left
            shape.addLine(to: CGPoint(x: 0, y: bounds.height)) // bottom-left
            shape.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height)) // left side of semi-circle
            shape.addArc(withCenter: CGPoint(x: bounds.width / 2, y: bounds.height), radius: radius, startAngle: -.pi, endAngle: 0, clockwise: true) // the semi-circle
            shape.addLine(to: CGPoint(x: bounds.width, y: bounds.height)) // bottom-right
            shape.addLine(to: CGPoint(x: bounds.width, y: 0)) // top-right
            shape.close()
            self.fillColor.setFill()
            shape.fill()
        }
    }
    
    let v = CustomView(frame: CGRect(x: 0, y: 0, width: 200, height: 150))
    v.backgroundColor = .clear
    v.fillColor = .blue
    PlaygroundPage.current.liveView = v
    

    This will work for any view size with properties to set the radius of the semi-circle and the fill color.


    Here's an update to your code that fixes your use of a mask:

    class CustomView2: UIView {
        var radius: CGFloat = 50
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            // Create a path with the semi-circle
            let path = UIBezierPath(
                arcCenter: CGPoint(
                    x: bounds.midX,
                    y: bounds.maxY
                ),
                radius: radius,
                startAngle: -.pi,
                endAngle: 0,
                clockwise: true
            )
            // Append the full rectangle
            path.append(UIBezierPath(rect: bounds))
            path.close()
    
            let maskLayer = CAShapeLayer()
            maskLayer.path = path.cgPath
            // Set the fill rule - this allows the rectangle to be filled and the semi-circle to be removed
            maskLayer.fillRule = .evenOdd
            layer.mask = maskLayer
        }
    }