3

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?

EDIT: The question that this is supposedly a duplicate of I had already attempted and was not able to get working. Perhaps I was doing it wrong or using it in the wrong context. I wasn't familiar with any of the given methods and also the use of pointers made it seem a bit outdated. The accepted answer does a much better job of explaining how this can be implemented using much more widely used UIBezierPaths and also within the context of a custom UIView.

Reilem
  • 345
  • 4
  • 10
  • Reilem - It works if you want to use Matt's two-paths with even-odd fill rule to draw path of everything that isn't masked. I personally find it counter-intuitive when I can just draw the path of the mask, but to each his own. Anyway, I updated my answer with both approaches. – Rob Nov 04 '16 at 06:35

1 Answers1

15

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.midX, y: bounds.minY)
        path.addArc(withCenter: center, radius: radius, startAngle: .pi, endAngle: 0, clockwise: false)
        path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
        path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
        path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
        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


If you wanted to use the "cut a hole" technique suggested in the duplicative answer, the updateMask method would be:

private func updateMask() {
    let center = CGPoint(x: bounds.midX, y: bounds.minY)

    let path = UIBezierPath(rect: bounds)
    path.addArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)

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

    layer.mask = mask
}

I personally find the path within a path with even-odd rule to be a bit counter-intuitive. Where I can (such as this case), I just prefer to just draw the path of the mask. But if you need a mask that has a cut-out, this even-odd fill rule approach can be useful.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 2
    Thank you so much for this! A very nice answer to my question, I wasn't even aware paths could be used this way so thanks for pointing that out to me. I've already put it into my code and it looks like it's all going to work out. I've put this as the accepted answer. :) – Reilem Nov 03 '16 at 17:55