0

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?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
John DoeDoe
  • 133
  • 1
  • 7

1 Answers1

2

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
    }
}
HangarRash
  • 7,314
  • 5
  • 5
  • 32
  • Thanks so much for this! This is the first sample of Playgrounds code doing simple pixel style graphics I could find anywhere that works. Now I have somewhere to start from! – hippietrail Aug 02 '23 at 04:48