Applying a CAShapeLayer mask to a UIView
Asked Answered
S

1

8

I am using this code to apply a layer to a UIview:

mask = [[CAShapeLayer alloc] init];
mask.frame = baseView.layer.bounds;
CGRect biggerRect = CGRectMake(mask.frame.origin.x, mask.frame.origin.y, mask.frame.size.width, mask.frame.size.height);
CGRect smallerRect = CGRectMake(0.0f, 0.0f, 0.0f, 0.0f);

UIBezierPath *maskPath = [UIBezierPath bezierPath];
[maskPath moveToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMinY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMaxY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(biggerRect), CGRectGetMaxY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(biggerRect), CGRectGetMinY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMinY(biggerRect))];

[maskPath moveToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMinY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMaxY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(smallerRect), CGRectGetMaxY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(smallerRect), CGRectGetMinY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMinY(smallerRect))];

mask.path = maskPath.CGPath;
[mask setFillRule:kCAFillRuleEvenOdd];
mask.fillColor = [[UIColor blackColor] CGColor];
baseView.layer.mask = mask;

Since the rectangle I want to cut changes, I was wonderning if there was a quick way to change the size of the mask instead of just removing it from the UIview and re-applying it with different dimensions:

e.g.

[mask removeFromSuperlayer];

and...

mask = [[CAShapeLayer alloc] init];
mask.frame = baseView.layer.bounds;
CGRect biggerRect = CGRectMake(mask.frame.origin.x, mask.frame.origin.y, mask.frame.size.width, mask.frame.size.height);
CGRect smallerRect = CGRectMake(0.0f, 100.0f, 200.0f, 200.0f);

UIBezierPath *maskPath = [UIBezierPath bezierPath];
[maskPath moveToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMinY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMaxY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(biggerRect), CGRectGetMaxY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(biggerRect), CGRectGetMinY(biggerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMinY(biggerRect))];

[maskPath moveToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMinY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMaxY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(smallerRect), CGRectGetMaxY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(smallerRect), CGRectGetMinY(smallerRect))];
[maskPath addLineToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMinY(smallerRect))];

mask.path = maskPath.CGPath;
[mask setFillRule:kCAFillRuleEvenOdd];
mask.fillColor = [[UIColor blackColor] CGColor];
baseView.layer.mask = mask;
Sword answered 26/3, 2013 at 15:19 Comment(0)
L
11

If you are asking whether you can reshape the mask without having to draw the path each time, unfortunately not. You can keep the mask shape the same and perform transformations on the mask by setting the transform property of the CAShapeLayer but if you need the mask to take a different shape your going to have to draw that shape by creating a new path. BUT there is no need to remove the mask from the UIView and create an entirely new one each time you need its shape to change. Simply setting the path property of the CAShapeLayer while it is still masking the UIView is sufficient for the changes to take place:

- (void)remask
{
    CAShapeLayer *mask = (CAShapeLayer *)self.view.layer.mask;

    CGRect biggerRect = CGRectMake(mask.frame.origin.x, mask.frame.origin.y, mask.frame.size.width, mask.frame.size.height);
    CGRect smallerRect = CGRectMake(0.0f, 100.0f, 200.0f, 200.0f);

    UIBezierPath *maskPath = [UIBezierPath bezierPath];
    [maskPath moveToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMinY(biggerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMaxY(biggerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(biggerRect), CGRectGetMaxY(biggerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(biggerRect), CGRectGetMinY(biggerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMinX(biggerRect), CGRectGetMinY(biggerRect))];

    [maskPath moveToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMinY(smallerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMaxY(smallerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(smallerRect), CGRectGetMaxY(smallerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMaxX(smallerRect), CGRectGetMinY(smallerRect))];
    [maskPath addLineToPoint:CGPointMake(CGRectGetMinX(smallerRect), CGRectGetMinY(smallerRect))];

    mask.path = maskPath.CGPath;
}

On a side note, are you aware that instead of drawing each rectangle manually you could create the mask path using the following code:

mask = [[CAShapeLayer alloc] init];
mask.frame = baseView.layer.bounds;
CGRect biggerRect = CGRectMake(mask.frame.origin.x, mask.frame.origin.y, mask.frame.size.width, mask.frame.size.height);
CGRect smallerRect = CGRectMake(0.0f, 100.0f, 200.0f, 200.0f);

CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddRect(maskPath, NULL, biggerRect);
CGPathAddRect(maskPath, NULL, smallerRect);

mask.path = maskPath;
[mask setFillRule:kCAFillRuleEvenOdd];
mask.fillColor = [[UIColor blackColor] CGColor];
baseView.layer.mask = mask;

CGPathRelease(maskPath);

I thought I'd just point it out as it may be of convenience to you in the future.

Litman answered 28/3, 2013 at 20:40 Comment(2)
Seems great, but whats the difference?Sword
@Sword Well the result is identical but other than that: less lines of code + arguably more readable/obvious + uses lightweight C API instead of 'heavyweight' UIBezierPath object so it's probably a tiny bit more performant. Just pointing out the 'AddRect' functions for convenience in the future. In reality I just needed something to accompany the bad news of my answer that want you'd like to do isn't directly possible haha.Litman

© 2022 - 2024 — McMap. All rights reserved.