How to apply multiple masks to UIView
Asked Answered
B

3

9

I have a question about how to apply multiple masks to a UIView that already has a mask.

The situation:
I have a view with an active mask that creates a hole in its top left corner, this is a template UIView that is reused everywhere in the project. Later in the project I would like to be able to create a second hole but this time in the bottom right corner, this without the need to create a completely new UIView.

The problem:
When I apply the bottom mask, it of course replaces the first one thus removing the top hole ... Is there a way to combine them both? And for that matter to combine any existing mask with a new one?

Thank you in advance!

Becnel answered 6/6, 2018 at 11:38 Comment(0)
B
9

Based on @Sharad's answer, I realised that re-adding the view's rect would enable me to combine the original and new mask into one.

Here is my solution:

func cutCircle(inView view: UIView, withRect rect: CGRect) {

    // Create new path and mask
    let newMask = CAShapeLayer()
    let newPath = UIBezierPath(ovalIn: rect)

    // Create path to clip
    let newClipPath = UIBezierPath(rect: view.bounds)
    newClipPath.append(newPath)

    // If view already has a mask
    if let originalMask = view.layer.mask,
        let originalShape = originalMask as? CAShapeLayer,
        let originalPath = originalShape.path {

        // Create bezierpath from original mask's path
        let originalBezierPath = UIBezierPath(cgPath: originalPath)

        // Append view's bounds to "reset" the mask path before we re-apply the original
        newClipPath.append(UIBezierPath(rect: view.bounds))

        // Combine new and original paths
        newClipPath.append(originalBezierPath)

    }

    // Apply new mask
    newMask.path = newClipPath.cgPath
    newMask.fillRule = kCAFillRuleEvenOdd
    view.layer.mask = newMask
}
Becnel answered 6/6, 2018 at 12:18 Comment(1)
Very simple solution...thanks! For those who want a rectangle instead of a circle, just change to: let newPath = UIBezierPath(rect: rect)Bismuth
R
4

This is code I have used in my project to create one circle and one rectangle mask in UIView, you can replace the UIBezierPath line with same arc code :

func createCircleMask(view: UIView, x: CGFloat, y: CGFloat, radius: CGFloat, downloadRect: CGRect){
    self.layer.sublayers?.forEach { ($0 as? CAShapeLayer)?.removeFromSuperlayer() }

    let mutablePath      = CGMutablePath()
    mutablePath.addArc(center: CGPoint(x: x, y: y + radius), radius: radius, startAngle: 0.0, endAngle: 2 * 3.14, clockwise: false)
    mutablePath.addRect(view.bounds)
    let path             = UIBezierPath(roundedRect: downloadRect, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 5, height: 5))
    mutablePath.addPath(path.cgPath)

    let mask             = CAShapeLayer()
    mask.path            = mutablePath
    mask.fillRule        = kCAFillRuleEvenOdd
    mask.backgroundColor = UIColor.clear.cgColor


    view.layer.mask      = mask
}

Pass your same UIView, it removes previous layers and applies new masks on same UIView.

Here mask.fillRule = kCAFillRuleEvenOdd is important. If you notice there are 3 mutablePath.addPath() functions, what kCAFillRuleEvenOdd does is, it first creates a hole with the arc then adds Rect of that view's bound and then another mask to create 2nd hole.

Rhapsodic answered 6/6, 2018 at 11:52 Comment(1)
Thanks-a-lot! Although your solution seems to remove and re-create all shapes I noticed that in between the arc and path you added the view's rect which is what I was missing! I added a more global solution that seems to work without having to know how the previous mask was created!Becnel
N
2

You can do something like the following, if you don't only have "simple shapes" but actual layers from e.g. other views, like UILabel or UIImageView.

let maskLayer = CALayer()
maskLayer.frame = viewToBeMasked.bounds
maskLayer.addSublayer(self.imageView.layer)
maskLayer.addSublayer(self.label.layer)

viewToBeMasked.layer.mask = maskLayer

So basically I just create a maskLayer, that contains all the other view's layer as sublayer and then use this as a mask.

Nutmeg answered 22/10, 2020 at 7:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.