Reilem Reilem - 1 month ago 29
Swift Question

Cut a circle out of a UIView using mask

In my app I have a square UIView and I want to cut a hole/notch out of the top of. All the tutorials online are all the same and seemed quite straightforward but every single one of them always delivered the exact opposite of what I wanted.

For example this is the code for the custom UIView:

class BottomOverlayView: UIView {

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawCircle()
}

fileprivate func drawCircle(){

let circleRadius: CGFloat = 80
let topMidRectangle = CGRect(x: 0, y: 0, width: circleRadius*2, height: circleRadius*2)

let circle: CAShapeLayer = CAShapeLayer()
circle.position = CGPoint(x: (frame.width/2)-circleRadius, y: 0-circleRadius)
circle.fillColor = UIColor.black.cgColor
circle.path = UIBezierPath(ovalIn: topMidRectangle).cgPath
circle.fillRule = kCAFillRuleEvenOdd

self.layer.mask = circle
self.clipsToBounds = true
}
}


Here is what I hope to achieve (the light blue is the UIView, the dark blue is the background):

What I want

But here is what I get instead. (Every single time no matter what I try)

What I get

I'm not sure how I would achieve this, aside from making a mask that is already the exact shape that I need. But if I was able to do that then I wouldn't be having this issue in the first place. Does anyone have any tips on how to achieve this?

Rob Rob
Answer

I'd suggest drawing a path for your mask, e.g. in Swift 3

//  BottomOverlayView.swift

import UIKit

@IBDesignable
class BottomOverlayView: UIView {

    @IBInspectable
    var radius: CGFloat = 100 { didSet { updateMask() } }

    override func layoutSubviews() {
        super.layoutSubviews()

        updateMask()
    }

    private func updateMask() {
        let path = UIBezierPath()
        path.move(to: bounds.origin)
        let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2.0, y: bounds.origin.y)
        path.addArc(withCenter: center, radius: radius, startAngle: CGFloat.pi, endAngle: 0, clockwise: false)
        path.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: bounds.origin.y))
        path.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: bounds.origin.y + bounds.size.height))
        path.addLine(to: CGPoint(x: bounds.origin.x, y: bounds.origin.y + bounds.size.height))
        path.close()

        let mask = CAShapeLayer()
        mask.path = path.cgPath

        layer.mask = mask
    }
}

Note, I tweaked this to set the mask in two places:

  • From layoutSubviews: That way if the frame changes, for example as a result of auto layout (or by manually changing the frame or whatever), it will update accordingly; and

  • If you update radius: That way, if you're using this in a storyboard or if you change the radius programmatically, it will reflect that change.

So, you can overlay a half height, light blue BottomOverlayView on top of a dark blue UIView, like so:

enter image description here

That yields:

enter image description here