iOS invert mask in drawRect
Asked Answered
C

9

30

With the code below, I am successfully masking part of my drawing, but it's the inverse of what I want masked. This masks the inner portion of the drawing, where I would like to mask the outer portion. Is there a simple way to invert this mask?

myPath below is a UIBezierPath.

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
CGPathRelease(maskPath);
self.layer.mask = maskLayer;
Cryptozoic answered 19/1, 2013 at 6:23 Comment(1)
On a deeper level this question is asking about fill rules, which determine which elements of intersecting paths are masked. I have found this link useful for those looking for more generalized answers and for my own understanding: calayer.com/core-animation/2016/05/22/…Analgesic
C
42

With even odd filling on the shape layer (maskLayer.fillRule = kCAFillRuleEvenOdd;) you can add a big rectangle that covers the entire frame and then add the shape you are masking out. This will in effect invert the mask.

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddRect(maskPath, NULL, someBigRectangle); // this line is new
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
maskLayer.fillRule = kCAFillRuleEvenOdd;         // this line is new
CGPathRelease(maskPath);
self.layer.mask = maskLayer;
Calcific answered 9/9, 2013 at 9:59 Comment(3)
May be you can answer this question too: #30360889Sake
was CGPathRelease(maskPath) removed? its working but can i get a memory leak? (Swift 2.2, iOS 9.0) Couldn't find any reference to it.Iq
That's correct, it won't cause a leak in Swift code. Core Foundation objects are automatically memory managed in Swift. See developer.apple.com/library/ios/documentation/Swift/Conceptual/…Clo
I
19

For Swift 3.0

func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
    let maskLayer = CAShapeLayer()
    let path = CGMutablePath()
    if (invert) {
        path.addRect(viewToMask.bounds)
    }
    path.addRect(maskRect)

    maskLayer.path = path
    if (invert) {
        maskLayer.fillRule = kCAFillRuleEvenOdd
    }

    // Set the mask of the view.
    viewToMask.layer.mask = maskLayer;
}
Ilium answered 14/2, 2017 at 23:21 Comment(0)
S
9

Based on the accepted answer, here's another mashup in Swift. I've made it into a function and made the invert optional

class func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
    let maskLayer = CAShapeLayer()
    let path = CGPathCreateMutable()
    if (invert) {
        CGPathAddRect(path, nil, viewToMask.bounds)
    }
    CGPathAddRect(path, nil, maskRect)

    maskLayer.path = path
    if (invert) {
        maskLayer.fillRule = kCAFillRuleEvenOdd
    }

    // Set the mask of the view.
    viewToMask.layer.mask = maskLayer;
}
Spiritualty answered 20/10, 2015 at 18:23 Comment(0)
A
7

Swift 5

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let red = UIView(frame: view.bounds)
        view.addSubview(red)
        view.backgroundColor = UIColor.cyan
        red.backgroundColor = UIColor.red
        red.mask(CGRect(x: 50, y: 50, width: 50, height: 50), invert: true)
    }
}

extension UIView{

    func mask(_ rect: CGRect, invert: Bool = false) {
        let maskLayer = CAShapeLayer()
        let path = CGMutablePath()
        if (invert) {
            path.addRect(bounds)
        }
        path.addRect(rect)
        maskLayer.path = path
        if (invert) {
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
        }
        // Set the mask of the view.
        layer.mask = maskLayer
    }
}

666

Thanks @arvidurs

Ashurbanipal answered 4/9, 2019 at 8:33 Comment(1)
Excellent. Thanks. The gift that keeps on giving. :)Wileywilfong
S
6

Here's my Swift 4.2 solution that allows a corner radius

extension UIView {

    func mask(withRect maskRect: CGRect, cornerRadius: CGFloat, inverse: Bool = false) {
        let maskLayer = CAShapeLayer()
        let path = CGMutablePath()
        if (inverse) {
            path.addPath(UIBezierPath(roundedRect: self.bounds, cornerRadius: cornerRadius).cgPath)
        }
        path.addPath(UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath)

        maskLayer.path = path
        if (inverse) {
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
        }

        self.layer.mask = maskLayer;
    }

}
Sevier answered 18/4, 2019 at 8:11 Comment(1)
i think it should be 0 like so roundedRect: self.bounds, cornerRadius: 0Woodpecker
R
3

For Swift 4.2

func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
    let maskLayer = CAShapeLayer()
    let path = CGMutablePath()
    if (invert) {
        path.addRect(viewToMask.bounds)
    }
    path.addRect(maskRect)

    maskLayer.path = path
    if (invert) {
        maskLayer.fillRule = .evenOdd
    }

    // Set the mask of the view.
    viewToMask.layer.mask = maskLayer;
}
Ramie answered 12/12, 2018 at 15:56 Comment(0)
T
1

There are many great answers, all of them using even-odd rule for resolving interior and exterior surfaces. Here is Swift 5 approach with non-zero rule:

extension UIView {
    func mask(_ rect: CGRect, cornerRadius: CGFloat, invert: Bool = false) {
        let maskLayer = CAShapeLayer()
        let path = CGMutablePath()
        
        if (invert) {
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: 0, y: bounds.height))
            path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
            path.addLine(to: CGPoint(x: bounds.width, y: 0))
            path.addLine(to: .zero)
        }
        
        path.addPath(UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath)
        maskLayer.path = path
        layer.mask = maskLayer
    }
}

For masking operation it is pretty straight forward. We draw rect and use it as masking path.

For inverted masking we use bounds of the view that is going to be masked. Then we draw it counter clockwise. After that we add UIBezierPath rect, which is by default drawn clockwise. That way any point p inside the rect will have one clockwise intersection and one counter clockwise intersection, leading to total winding number of zero, making that point an exterior point.

Turbojet answered 15/9, 2022 at 21:52 Comment(0)
A
1

For 2023. All answers currently here seem wrong as they don't adjust the mask when the layout changes (or - just one example - when the view is animating in size or shape).

It's very simple ..

1. have a layer (perhaps just a square of color, an image, whatever)

lazy var examp: CALayer = {
    let l = CALayer()
    l.background = UIColor.blue.cgColor
    l.mask = shapeAsMask
    layer.addSublayer(l)
    return l
}()

Note that examp has its mask set already.

2. Whenever you want to mask, you of course need a masking layer on hand

lazy var shapeAsMask: CAShapeLayer = {
    let s = CAShapeLayer()
    s.fillRule = .evenOdd
    layer.addSublayer(s)
    layer.mask = s
    return s
}()

Note that the fillrule is set as needed.

3. Now make a shape with a bezier curve. Let's just make a circle:

override func layoutSubviews() {
    super.layoutSubviews()
    
    examp.frame = bounds

    let i = bounds.width * 0.30
    let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
    ...
}

So that's a small circle in the middle of the view.

If you want to "have the shape" it's just

override func layoutSubviews() {
    super.layoutSubviews()
    
    fill.frame = bounds

    let i = bounds.width * 0.30
    let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
    
    shapeAsMask.path = thing.cgPath
}

If you want to "have the INVERSE OF the shape" it's just

override func layoutSubviews() {
    super.layoutSubviews()
    
    fill.frame = bounds

    let i = bounds.width * 0.30
    let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))

    let p = CGMutablePath()
    p.addRect(bounds)
    p.addPath(thing.cgPath)

    shapeAsMask.path = p
}

In short, the "negative" of this ..

let thing = UIBezierPath( ... some path

is just this:

let neg = CGMutablePath()
neg.addRect(bounds)
neg.addPath(thing.cgPath)

So that's it.

Don't forget that ...

Anytime you have a mask (or a layer!) you have to set it in layoutSubviews. (That's why layoutSubviews is named layoutSubviews !)

Arborvitae answered 9/2, 2023 at 20:21 Comment(0)
T
0

In order to invert mask you can to something like this

  1. Here I have mask of crossed rectancles like

     let crossPath =  UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5))
              crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
                let crossMask = CAShapeLayer()
                crossMask.path = crossPath.cgPath
                crossMask.backgroundColor = UIColor.clear.cgColor
                crossMask.fillRule = .evenOdd
    
  2. And here I add 3rd rectancle around my crossed rectancles so using .evenOdd it takes area that is equal to (New Rectancle - Old Crossed Rectangle) so in other words outside area of crossed rectancles

      let crossPath = UIBezierPath(rect: cutout.insetBy(dx: -5, dy: -5) )
        crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5)))
        crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
        let crossMask = CAShapeLayer()
        crossMask.path = crossPath.cgPath
        crossMask.backgroundColor = UIColor.clear.cgColor
        crossMask.fillRule = .evenOdd
    

enter image description here

Truculent answered 31/1, 2020 at 22:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.