Search code examples
swiftuibezierpathcashapelayer

Make parts of UIView transparent by using a masking layer


I am trying to achieve something like this:

enter image description here

Where the pink part is fixed and the grey area could possibly scroll. I have the following view structure:

enter image description here

I currently have this code:

extension FloatingPoint {

    var degreesToRadians: Self { return self * .pi / 180 }
    var radiansToDegrees: Self { return self * 180 / .pi }

} 

@IBDesignable
class MaskView: UIView {

    let startAngle: CGFloat = 180
    let endAngle: CGFloat = 0

    override func layoutSubviews() {
        super.layoutSubviews()

        let multiplier: CGFloat = (frame.size.height * 3)
        let maskLayer = CAShapeLayer()
        maskLayer.path = UIBezierPath(arcCenter: CGPoint(x: frame.size.width/2, y: frame.size.height * multiplier),
                                  radius: frame.size.height * multiplier,
                                  startAngle: startAngle.degreesToRadians,
                                  endAngle: endAngle.degreesToRadians,
                                  clockwise: true).cgPath

        layer.mask = maskLayer
    }

}

The Mask View is set to have the grey background color.

The problem that I have is that the circular part is not transparent. Changing the color to UIColor.clear will only make the circular part to disappear. Am I fundamentally doing the wrong approach for this? Any help would be appreciated, thanks.


Solution

  • First of all, we need to create a subclass from UIView (which you already did). But here is the code that I used:

    private extension FloatingPoint {
        var degreesToRadians: Self { return self * .pi / 180 }
        var radiansToDegrees: Self { return self * 180 / .pi }
    }
    
    @IBDesignable class MaskView: UIView {
        let startAngle: CGFloat = 180
        let endAngle: CGFloat = 0
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            // The multiplier determine how big the circle is
            let multiplier: CGFloat = 3.0
            let radius: CGFloat = frame.size.width * multipler
            let maskLayer = CAShapeLayer()
            let arcCenter = CGPoint(x: frame.size.width / 2, y: radius)
            maskLayer.path = UIBezierPath(arcCenter: arcCenter,
                                          radius: radius,
                                          startAngle: startAngle.degreesToRadians,
                                          endAngle: endAngle.degreesToRadians,
                                          clockwise: true).cgPath
            layer.mask = maskLayer
        }
    }
    

    Then you can add a MaskView as a subView in the ViewController. Make sure select the view assigned the class MaskView in the storyboard:

    Now we have a very simply view hierarchy:

    Compiles the code and it's looking great:

    If you want a scrollable subview that is masked, add it as a subview of the maskView. Here is how the view hierarchy looks after this:

    And finally, this is how it looks running in the simulator:

    Ref: https://github.com/yzhong52/Example-LayerMask