56

I want to know how to simply mask the visible area of a UIView of any kind. All the answers/tutorials I've read so far describe masking with an image, gradient or creating round corners which is way more advanced than what I am after.

Example: I have a UIView with the bounds (0, 0, 100, 100) and I want to cut away the right half of the view using a mask. Therefore my mask frame would be (0, 0, 50, 100).

Any idea how to do this simply? I don't want to override the drawrect method since this should be applicable to any UIView.

I've tried this but it just makes the whole view invisible.

CGRect mask = CGRectMake(0, 0, 50, 100);
UIView *maskView = [[UIView alloc] initWithFrame:mask];
viewToMask.layer.mask = maskView.layer;
Accatyyc
  • 5,798
  • 4
  • 36
  • 51
  • I'm not sure if I understand, but why don't you just change the frame of your view to mask? Eg viewToMask.frame = mask; (or the inverse if that's what you want to show.) – JamesSugrue Jul 09 '12 at 08:01
  • That won't work since the views may have stretchable images which should be cut off, or controls which should still be usable even if masked out. Changing the frame would change the image stretch or make the controls unusable. – Accatyyc Jul 09 '12 at 08:02
  • 6
    Check if this helps http://stackoverflow.com/questions/7241966/masking-uiview-based-on-cgrect – msk Jul 09 '12 at 08:04
  • It helped :) Adding the way I went with as an answer. – Accatyyc Jul 09 '12 at 08:26
  • or this one, mask view by view http://stackoverflow.com/questions/6805444/masking-a-uiview – Volodymyr B. Jul 09 '12 at 08:33

7 Answers7

130

Thanks to the link from MSK, this is the way I went with which works well:

// Create a mask layer and the frame to determine what will be visible in the view.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGRect maskRect = CGRectMake(0, 0, 50, 100);

// Create a path with the rectangle in it.
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);

// Set the path to the mask layer.
maskLayer.path = path;

// Release the path since it's not covered by ARC.
CGPathRelease(path);

// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
Ben Baron
  • 14,496
  • 12
  • 55
  • 65
Accatyyc
  • 5,798
  • 4
  • 36
  • 51
  • This answer provides a good way to do an inverse mask w/UIBezierPath (though using a circle in the example) -- http://stackoverflow.com/a/15377207/437492 – Vasiliy Kulakov Mar 12 '14 at 13:26
28

Thanks for answers guys.

In case someone can't find suitable answer on SO for this question for hours, like i just did, i've assembled a working gist in Swift 2.2 for masking/clipping UIView with CGRect/UIBezierPath:

https://gist.github.com/Flar49/7e977e81f1d2827f5fcd5c6c6a3c3d94

extension UIView {

    func mask(withRect rect: CGRect, inverse: Bool = false) {
        let path = UIBezierPath(rect: rect)
        let maskLayer = CAShapeLayer()

        if inverse {
            path.append(UIBezierPath(rect: self.bounds))
            maskLayer.fillRule = kCAFillRuleEvenOdd
        }

        maskLayer.path = path.cgPath

        self.layer.mask = maskLayer
    }

    func mask(withPath path: UIBezierPath, inverse: Bool = false) {
        let path = path
        let maskLayer = CAShapeLayer()

        if inverse {
            path.append(UIBezierPath(rect: self.bounds))
            maskLayer.fillRule = kCAFillRuleEvenOdd
        }

        maskLayer.path = path.cgPath

        self.layer.mask = maskLayer
    }
}

Usage:

let viewSize = targetView.bounds.size
let rect = CGRect(x: 20, y: 20, width: viewSize.width - 20*2, height: viewSize.height - 20*2)

// Cuts rectangle inside view, leaving 20pt borders around
targetView.mask(withRect: rect, inverse: true)

// Cuts 20pt borders around the view, keeping part inside rect intact
targetView.mask(withRect: rect)

Hope it will save someone some time in the future :)

Hashem Aboonajmi
  • 13,077
  • 8
  • 66
  • 75
Eugene Tartakovsky
  • 1,594
  • 17
  • 22
22

No need any mask at all. Just put it into a wrapper view with the smaller frame, and set clipsToBounds.

wrapper.clipsToBounds = true
Geri Borbás
  • 15,810
  • 18
  • 109
  • 172
  • Doesn't really work if you want to cut a control, since it won't respond to touches outside its parents frame. Also, this basically does the same thing as setting a mask, except that now you have a relative frame to worry about. – Accatyyc Oct 11 '13 at 12:50
  • I think it is more performant, as this one probably adjusts texture coordinates only under the hood, but apply mask probably does alpha blending. – Geri Borbás Oct 11 '13 at 14:36
  • You can always override -pointInside: to get responses outside the frame. Though, it may be really an overhead for this. – Geri Borbás Oct 11 '13 at 14:37
  • 1
    Much easier way! Thanks. – Eric Chen Aug 10 '14 at 17:15
3

Very simple example in a Swift ViewController, based on the accepted answer:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let redView = UIView(frame: view.bounds)
        view.addSubview(redView)

        view.backgroundColor = UIColor.green
        redView.backgroundColor = UIColor.red

        mask(redView, maskRect: CGRect(x: 50, y: 50, width: 50, height: 50))
    }

    func mask(_ viewToMask: UIView, maskRect: CGRect) {
        let maskLayer = CAShapeLayer()
        let path = CGPath(rect: maskRect, transform: nil)
        maskLayer.path = path

        // Set the mask of the view.
        viewToMask.layer.mask = maskLayer
    }
}

Output

enter image description here

Michal Šrůtek
  • 1,647
  • 16
  • 17
Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
1

An optional layer whose alpha channel is used as a mask to select between the layer's background and the result of compositing the layer's contents with its filtered background.

@property(retain) CALayer *mask

The correct way to do what you want is to create the maskView of the same frame (0, 0, 100, 100) as the viewToMask which layer you want to mask. Then you need to set the clearColor for the path you want to make invisible (this will block the view interaction over the path so be careful with the view hierarchy).

A-Live
  • 8,904
  • 2
  • 39
  • 74
  • Thanks for the answer! I think I found a simpler way which doesn't use alpha though, so I added my own answer. – Accatyyc Jul 09 '12 at 08:30
  • @Accatyyc Nice solution, i've edited it by renaming `maskLayer` and `maskRect` to `homeTeamMaskLayer` and `homeTeamMaskRect` so that it compiles :) Need to wait it to be approved though and thanks for update. – A-Live Jul 09 '12 at 08:53
  • Ah, thanks for the edit suggestion. I updated it the other way around though so it makes sense for other projects than mine :) – Accatyyc Jul 09 '12 at 09:02
0

Swift 5 , thanks @Dan Rosenstark

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))
    }
}

extension UIView{
    func mask(_ rect: CGRect){
        let mask = CAShapeLayer()
        let path = CGPath(rect: rect, transform: nil)
        mask.path = path
        // Set the mask of the view.
        layer.mask = mask
    }
}
dengST30
  • 3,643
  • 24
  • 25
-2

Setting MaskToBounds is not enough, for example, in scenario where you have UIImageView that is positioned inside RelativeLayout. In case that you put your UIImageView to be near top edge of the layout and then you rotate your UIImageView, it will go over layout and it won't be cropped. If you set that flag to true, it will be cropped yes, but it will also be cropped if UIImageView is on the center of layout, and that is not what you want.

So, determinating maskRect is the right approach.

Sagar Jain
  • 7,475
  • 12
  • 47
  • 83
jefimijana
  • 19
  • 3