8

I have a UIView that I want to mask with another UIView, punching a hole out of its center. Here's my viewDidLoad:

- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.viewToMask];
[self.view addSubview:self.theMask];

[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.viewToMask attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.viewToMask attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[cyan(200)]" options:0 metrics:nil views:@{@"cyan": self.viewToMask}]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[cyan(200)]" options:0 metrics:nil views:@{@"cyan": self.viewToMask}]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.theMask attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.viewToMask attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.theMask attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.viewToMask attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[mask(100)]" options:0 metrics:nil views:@{@"mask": self.theMask}]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[mask(100)]" options:0 metrics:nil views:@{@"mask": self.theMask}]];
}

It gives me exactly what I'm looking for, minus the masking:

views without masking

If I add one more line:

[self.viewToMask setMaskView:self.theMask];

both views disappear --- the small view (self.theMask) masks out the whole larger view (self.viewToMask) even though it's only half the size. Does anyone understand what's going on here? Can you not use UIView.maskView with Auto Layout?

jefflovejapan
  • 2,047
  • 3
  • 20
  • 34
  • 1
    I know when you use the pre-iOS 8 version of this (`-[CALayer maskLayer]`), you had to position the masked layer’s coordinate space, and the mask layer doesn’t exist in the normal view hierarchy. I wonder if something similar is going on here? – Zev Eisenberg Jan 28 '15 at 02:57
  • That's totally what's going on here. Thanks a lot. – jefflovejapan Jan 28 '15 at 03:41
  • So how did it work out? Can you use Auto Layout with a `maskView`? – Zev Eisenberg Jan 28 '15 at 03:47

3 Answers3

6

As Zev explained, the mask view lives outside of the ordinary view hierarchy and so can't be used together with Auto Layout. I got around this by placing it manually in my view controller's viewDidLayoutSubviews:

-(void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];
    CGRect viewToMaskRect = self.viewToMask.bounds;
    CGRect maskRect = CGRectMake(viewToMaskRect.origin.x + 50.0, viewToMaskRect.origin.y + 50.0, 100.0, 100.0);
    [self.theMask setFrame:maskRect];
    [self.viewToMask setMaskView:self.theMask];
}

proper masking

jefflovejapan
  • 2,047
  • 3
  • 20
  • 34
  • Cool. Although that doesn't look like the hole punched in the center you said you were going for…? – Zev Eisenberg Jan 28 '15 at 16:17
  • Yeah, masking is the inverse of what I thought (clear pixels let the subviews of the view you're masking show through). I used this solution to get the hole punch effect: http://stackoverflow.com/questions/8859285/uibezierpath-subtract-path – jefflovejapan Jan 28 '15 at 16:26
4

Based on Russ' answer I did my own research and got an alternative workaround.

The code sample below will mask the left and right side of the main view with 10pt. Important

class SomeViewController: UIViewController {

    override func viewDidLoad() {

        super.viewDidLoad()

        self.view.backgroundColor = UIColor.whiteColor()

        self.setupMaskView()
    }

    private func setupMaskView() {

        let maskView = UIView()
        maskView.translatesAutoresizingMaskIntoConstraints = false
        maskView.backgroundColor = UIColor(white: 0.0, alpha: 1.0)

        let maskContainerView = UIView()
        maskContainerView.addSubview(maskView)

        self.view.addSubview(maskContainerView)

        maskView.leftAnchor.constraintEqualToAnchor(self.view.leftAnchor, constant: 10).active = true
        maskView.widthAnchor.constraintEqualToAnchor(self.view.widthAnchor, constant: -20).active = true
        maskView.heightAnchor.constraintEqualToAnchor(self.view.heightAnchor).active = true

        self.view.maskView = maskContainerView // this will not work it we place this line above the constraint code !!!
        /* I have no idea why this does not work and why nesting is required, maybe some sort of bug? */
    }
}

This code is using AutoLayout syntax available from iOS 9.0 and Swift 2.0. (Written in Xcode 7 beta 4)

DevAndArtist
  • 4,971
  • 1
  • 23
  • 48
  • This is actually a very clever trick! As for the comment in code - you cannot set constraint when view is not in the hierarchy. – Andrzej Michnia Apr 27 '19 at 09:56
0

You can make auto layout work with your maskView by making the maskView just a normal subview (Eg. in a storyboard with relevant constraints) then only setting it as maskView in viewWillLayoutSubviews. Ie:

-(void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    self.viewToBeMasked.maskView = self.maskViewWithLayoutConstraints;
}
Russ
  • 31
  • 3
  • This does not work for me without nesting the mask view. It is kinda strange. :/ I'll post it as an alternative answer. – DevAndArtist Jul 31 '15 at 10:42