"Behind the scenes" difference between cornerRadius and bezierPathWithRoundedRect - ios

Premise: I am rounding the edges of a view with a solid background (to make something like an oval). There are many ways to do this, but the two most common I've see recommended are:
1) set the cornerRadius property on the view layer and bind the mask layer to the view layer bounds (through clipsToBounds or masksToBounds) (https://developer.apple.com/reference/quartzcore/calayer/1410818-cornerradius)
ex:
view.layer.cornerRadius = 2
view.layer.maskToBounds = true
2) create a Bezier Path with [bezierPathWithRoundedRect:byRoundingCorners:cornerRadii] and use it to create a mask layer which you set to the view's layer's mask. (https://developer.apple.com/reference/uikit/uibezierpath/1624368-bezierpathwithroundedrect)
ex:
UIBezierPath* bezierPath = [UIBezierPath bezierPathWithRoundedRect: view.bounds
byRoundingCorners:UIRectCornerAllCorners
cornerRadii: CGSizeMake(2, 2)];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.frame = view.bounds;
maskLayer.path = bezierPath.CGPath;
view.layer.mask = maskLayer;
Question: Both ways work well for me and I see no significant performance of one over the other (admittedly, I'm not using it heavily). In which situations should I use a Bezier path over setting the corner radius (assuming the corner radii are equal)? Do these two methods to creating rounded edges do the same thing behind the scenes? Are there certain platforms that won't support both solutions?
Best Regards

Related

How to set the contents of CAShapeLayer which has been shaped by UIBezierPath

I tried to add a mask layer on a UIImageView. This imageview hold an image and the mask layer hold another image by setting its contents. But when I add the mask layer onto the layer of imageview, the mask layer doesn't hold any image. The mask layer had been initialized by getter. The method adding mask layer is below:
- (void)circleButtonClicked{
self.maskLayer.contents = (id)[UIImage imageNamed:#"mask.jpg"].CGImage;
self.maskLayer.frame = CGRectMake(0, 0, kSCREEN_WIDTH, kSCREEN_WIDTH);
self.maskLayer.opacity = 0.5;
[self.imageView.layer addSublayer:self.maskLayer];
UIBezierPath * pPath = [UIBezierPath bezierPathWithArcCenter:self.imageView.center radius:(kSCREEN_WIDTH-100)/2 startAngle:0.0 endAngle:M_PI*2 clockwise:YES];
self.maskLayer.path = pPath.CGPath;
UIBezierPath * otherPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, kSCREEN_WIDTH, kSCREEN_WIDTH)];
self.maskLayer.path = otherPath.CGPath;
[otherPath appendPath:pPath];
self.maskLayer.path = otherPath.CGPath;
self.maskLayer.fillRule = kCAFillRuleEvenOdd;
}
Shape layers are used for displaying paths not images. There are three different ways to provide layer content (see Core Animation Programming Guide) but only one at a time.
There are some things in your code which I don't understand:
Your maskLayer is just a normal layer and not a mask. When you want to use it as mask, you should use the mask property of CALayer and not add it as sublayer.
Your layer should contain an image and a path, and should be a sublayer in an image view. I think its easier to use a image view and add it as subview to imageView.
If you want to use an image as mask, which should be possible, you shouldn't use a JPEG image as source. Only the alpha values of a mask layer are considered, but the alpha values of the whole JPEG image are one. Thus, using a JPEG image is equivalent to using a filled square, or applying no mask.

Attempting to mask a circle around an image not working

I have an image that I am attempting to mask a circle around so the image appears round. This somewhat works but the circle comes to a point on the top and bottom.
profileImageView.layer.cornerRadius = profileImageView.frame.size.width/2;
profileImageView.layer.masksToBounds = YES;
Should this code be drawing a perfect circle? It seems to draw a circle in one place but in two other places, its not working correctly.
I have had the best results masking the image view with a CAShapeLayer:
CGFloat radius = self.profileImageView.frame.size.width / 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius startAngle:0 endAngle:M_PI * 2.0 clockwise:TRUE];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
layer.lineWidth = 0;
self.profileImageView.layer.mask = layer;
Should this code be drawing a perfect circle?
Not necessarily. After all, the width and the height of this layer might not be the same. And even if they are, dividing by 2 might not give you a radius that fits perfectly into an integral number of points as they are mapped to pixels on the screen.
It really would be better, if what you want is a mask that's a circle, to give this layer an actual mask that is an actual circle. Misusing the corner radius as you are doing is just lazy (and, as you've discovered, it's error-prone).

How to create a CGPath which exactly match a CALayer bounds when the layer rotate or scale?

We have a CALayer which shows image with edit information. Such as rotate, scale and translate. I want to make a CGPath which exactly match the layer bounds. When the layer rotate, my path should also rotate and its four corners should match the CALayer's four corners. The CGPath is actually used as a gray mask to show the clipped area of the image.
I try following code, but it does not work.
f = self.imageLayer.frame;
t = self.imageLayer.affineTransform;
CGPathAddRect(path, &t, f);
The CALayer has its own CGAffineTransform. All edit information are applied via the CGAffineTransform.
Any hint will be appreciated, Thanks a lot.
If I got your question right you could be using UIBezierPath's usesEvenOddFillRule to cut around your image layer bounds dimming the rest of the visible screen. Basically you create a very large rectangular path (for some reasons CGRectInfinite doesn't work here) and cut out the area around your image layer. The trick is to use kCAFillRuleEvenOdd which flips what is considered inside and outside.
Something like should work:
let cutPath = UIBezierPath(roundedRect: imageLayer.bounds, cornerRadius: 0)
let clipPath = UIBezierPath(rect: CGRectMake(-10e6, -10e6, 10e7, 10e7)) // CGRectInfinite isn't working here ?
clipPath.appendPath(cutPath)
clipPath.usesEvenOddFillRule = true
let shape = CAShapeLayer()
shape.contentsScale = UIScreen.mainScreen().scale
shape.lineWidth = 2
shape.fillColor = UIColor(red:0.1, green:0.1, blue:0.1, alpha:0.5).CGColor
shape.fillRule = kCAFillRuleEvenOdd
shape.strokeColor = UIColor.whiteColor().CGColor
shape.path = clipPath.CGPath
imageLayer.addSublayer(shape)
// do a transformations here
imageLayer.transform = CATransform3DMakeRotation(10.0, 1.0, 1.0, 0.8)
Which results

Use Bezier Path as Clipping Mask

I am wondering if it is possible to clip a view to a Bezier Path. What I mean is that I want to be able to see the view only in the region within the closed Bezier Path. The reason for this is that I have the outline of an irregular shape, and I want to fill in the shape gradually with a solid color from top to bottom. If I could make it so that a certain view is only visible within the path then I could simply create a UIView of the color I want and then change the y coordinate of its frame as I please, effectively filling in the shape. If anyone has any better ideas for how to implement this that would be greatly appreciated. For the record the filling of the shape will match the y value of the users finger, so it can't be a continuous animation. Thanks.
Update (a very long time later):
I tried your answer, Rob, and it works great except for one thing. My intention was to move the view being masked while the mask remains in the same place on screen. This is so that I can give the impression of the mask being "filled up" by the view. The problem is that with the code I have written based on your answer, when I move the view the mask moves with it. I understand that that is to be expected because all I did was add it as the mask of the view so it stands to reason that it will move if the thing it's tied to moves. I tried adding the mask as a sublayer of the superview so that it stays put, but that had very weird results. Here is my code:
self.test = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.test.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.test];
UIBezierPath *myClippingPath = [UIBezierPath bezierPath];
[myClippingPath moveToPoint:CGPointMake(100, 100)];
[myClippingPath addCurveToPoint:CGPointMake(200, 200) controlPoint1:CGPointMake(self.screenWidth, 0) controlPoint2:CGPointMake(self.screenWidth, 50)];
[myClippingPath closePath];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
self.test.layer.mask = mask;
CGRect firstFrame = self.test.frame;
firstFrame.origin.x += 100;
[UIView animateWithDuration:3 animations:^{
self.test.frame = firstFrame;
}];
Thanks for the help already.
You can do this easily by setting your view's layer mask to a CAShapeLayer.
UIBezierPath *myClippingPath = ...
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;
You will need to add the QuartzCore framework to your target if you haven't already.
In Swift ...
let yourCarefullyDrawnPath = UIBezierPath( .. blah blah
let maskForYourPath = CAShapeLayer()
maskForYourPath.path = carefullyRoundedBox.CGPath
layer.mask = maskForYourPath
Just an example of Rob's solution, there's a UIWebView sitting as a subview of a UIView called smoothView. smoothView uses bezierPathWithRoundedRect to make a rounded gray background (notice on right). That works fine.
But if smoothView has only normal clip-subviews, you get this:
If you do what Rob says, you get the rounded corners in smoothView and all subviews ...
Great stuff.

Why is making two corners round slower than make all corners round?

Edit: Language updated to improve readability.
I made an image view with 2 rounded corners like this:
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.photoImageView.bounds byRoundingCorners:UIRectCornerBottomLeft|UIRectCornerBottomRight cornerRadii:CGSizeMake(10, 10)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
self.photoImageView.layer.mask = maskLayer;
But it is slower than making all the corners round using this code.
self.photoImageView.layer.cornerRadius = 10;
Would anyone know what why and how I can improve my '2 corner' code please?
Your code is adding another stage to the drawing. Normally, the background is drawn (with the given cornerRadius) directly to the target, but with a mask specified, it is drawn to a temporary surface, then copied to the target using the mask.
There isn't any built-in functionality for only rounding some background corners in the standard CALayer object.
I do wonder how slow "slower" really is; is this premature optimisation?

Resources