iOS Understanding Layer Masking - ios

I am having some difficulty understanding on how layer masking works. Right now, I have a UIView with UILabels on it. I picture two layers - one with the UIView in the back and one for the labels on top. If I mask the UIView layer, the labels will be affected by the mask too.
The UILabels are children of the parent UIView, so I can understand a parent mask affecting the children as well.
However, when I look at it in terms of layers, it doesn't seem to make sense. Why does masking the deepest layer affect those on top?

Think of layers as sheets of paper. Think of the view's layer as a big sheet of white paper. As you figured out, the labels' layers are children of the view's layers. To relate, think of the labels' layers being strips of paper glued onto the big view's layer-sheet.
Let's say you wish to mask the layer with a circle. To translate that into our little analogy, you wish to cover the big view's layer-sheet with Harry Potter's invisibility cloak, with a circle shaped hole in it.
To do that, you'd cut the invisibility cloak to the same size as that of your view's layer-sheet.
cloakLayer.frame = bigViewLayer.frame;
Then, you'd carefully cut out a circle from it.
cloakLayer.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 0) radius:15.0 startAngle:0.0 endAngle:2 * M_PI clockwise:YES];
cloakLayer.fillColor = [UIColor blackColor].CGColor; // the hole
Then, you'd paste this cloak-with-a-hole onto your big view's layer-sheet, carefully aligning the edges.
bigViewLayer.mask = cloakLayer;
What's gonna go invisible? Anything on the sheet (because the cloak was cut to the sheet's dimensions) that doesn't fall into the circle you removed from the cloak. That's the mask property.
Let's talk about the masksToBounds property.
Let's say while pasting the strips of label layer-sheets onto the big view's layer-sheet, you decided to place only half of the strip on the sheet, and made the rest hang off the edge(s).
Let's say you set masksToBounds to YES. What the gods of decoupage would do now is neatly cut off the parts of your labels' strips that are not within the edges of the big view's layer-sheet. That's the masksToBounds property.
Let's talk about borders. This is simple. Just pick a sharpie of borderColor whose nib is borderWidth points wide, and carefully draw on the edges of the view's layer-sheet. That's it.
I hope you get things now and can make your own analogies for other properties of the wonderful CALayer.

As you said, the UIView is the parent, and the UILabels are the children. When it comes time to update the screen, the UIView starts with a blank canvas. It draws itself into the canvas, and then has the children draw themselves into the canvas. When the children are drawing, they are subject to constraints imposed by the parent, e.g. clipping and masking.

iOS CALayer.mask
[CALayer]
[iOS CALayer.masksToBounds]
CALayer has a mask property which is CALayer, which applies mask's alpha channel to mask parent's layer.
layer + mask = masked layer

Related

Smootly mooving a hole in UIImage?

I have a UIImageView displaing an image. This view's layer is masked with CAShapeLayer in order to create circular "hole" in the image. To create the hole I use UIBezierPath with .usesEvenOddFillRule = true.
It works fine when static. But I need that hole to move with user finger. To do that I create new UIBezierPath with even-odd rule each time user moves their finger. On smaller phones with smaller images it looks OK but on iPhone 6 Plus it is choppy.
Any ideas on how to make it smooth are very wellcome. I cannot just move the frame of masking CAShapeLayer - it would move the hole bot also hide some edges of the image. So the only way is to change its .path each time user moves finger and that is slow.
EDIT: matt's answer would work in some scenarios but not in my case: I am not displaying the whole image only a part of it defined by UIBezierPath. This part is most often oval (but can be rectangular or rounded rectangle) and it has "hole" cut in it. While the hole is mowing with users finger the displayed part/shape of the image does not change - it is static.
The ineficient solution that was in place so far wa:
Create UIBezierPath with boundary of displayed part of the image
Set 'even off fill rule' on it
Add UIBezierPath of the hole to it
Set it as path of CAShapeLayer with some opaque fill color
Use that CAShapeLayer as mask of the UIImageView
This procedure was repeated each time a user moved their finger. I cannot simply move the whole mask layer as that would also change the part of the image being displayed. I what it to stay static and move only the hole in it.
it would move the hole bot also hide some edges of the image
Well, I don't agree. Moving the mask is exactly the way to do this. I don't see why you think there's a problem with that. Perhaps the issue is merely that you have not made the mask layer big enough. It does not have to be the same size as the layer it is masking. In this case, it needs to be about 9 times the size of the masked layer (3 horizontal and 3 vertical), so that it will continue to cover the masked the layer no matter how far in any direction the user slides it.

Is it possible to have a variable height UIView mask layer NOT stretch, and perhaps just sit at the top? (For example, a "torn paper" style edge?)

I can't for the life of me figure out how to create such an effect on a UIView that has dynamic height. I want to have the top have a "torn paper" style edge, but the view can be of dynamic height, so when I set the torn paper mask at the top, it gets stretched the full length of the view.
Can I say, "stick to the top", or perhaps designate a portion that is supposed to stretch? Preferably with layers but iOS 8's maskView property works too.
You can set the frame of the layer's mask to anything you want. So that's one way to position it.
Or you could (probably, I haven't tried yet) use a stretchable image view as the mask (- resizableImageWithCapInsets:resizingMode:). That should keep the torn paper edge from stretching.

iOS: How to fill a rectangle except in one area?

I've used the CGContext based routine to fill a rectangle, but this time I actually want to fill all of a rectangle EXCEPT one part of it. I know this is possible in other drawing systems for other platofrm but can it be done with core graphics on iOS?
Both borderWidth and cornerRadius are animatable properties. So you could just animate them directly (using CABasicAnimation). In that rendering, you'd go from no border and no corners to having a border and corners in an animated way.
If you want to animate the rectangular tracing of the border, you'd need to use a CAShapeLayer (because the end of its path is animatable) and provide the illusion that way.

iOS animate clipToBounds, masksToBounds, or similar

I have an image view that starts out cropping it's image with clipsToBounds and content mode set as "scale aspect fill", and I want it to "enlarge" the image to the whole image. If clipsToBounds=NO was an animatable property, that would be exactly what I want, which it does not seem to be. Is there a way to animate that?
If not, another way would be resizing the view so it is the same width-height ratio as the image's size, while keeping it no smaller than the image view was to begin with (i.e. minimal increate to height or width, no decrease to either). I'm not sure the best approach to doing this, considering the image could be much bigger or smaller than the image view, and the image's width-height ratio could be just about anything (but it will usually be an iOS device camera photo).
UPDATE 1: It seems like layer.masksToBounds would work, the documentation says that it is animatable, but my code does not seem to work:
CABasicAnimation *layerAnim = [CABasicAnimation animationWithKeyPath:#"masksToBounds"];
layerAnim.fromValue = #(YES);
layerAnim.toValue = #(NO);
layerAnim.removedOnCompletion = YES;
layerAnim.duration = 1.0;
[_imageView.layer addAnimation:layerAnim forKey:#"masksToBounds"];
I am running this layer animation at the same time as a UIView block animation that is changing the frame and transform of the image view, if that matters.
UPDATE 2: I did not have this core animation in the UIView animation block, which according to the documentation, it should be. I have moved the above code into the animation block, but it is still not animating (change happens instantly). I'm beginning to think that "animatable" simply means it can be placed in an animation, not that it will actively animate over time.
So you don't want your image to grow, but you want it to be clipped at first, and then the outer pixels are exposed?
You can do that using Core Animation and a layer mask.
Here's what you do:
Set the image view's clipsToBounds to FALSE, so the image would fully display if you let it.
Create a CAShapeLayer that's the size of the whole image. Create a rectangular bezier path that's the size of the initial image. Install that bezier path's CGPath as the path of the shape layer.
Set the shape layer's fill color to an opaque color
Then install the shape layer as the mask of your image view's layer. That will cause the image to be clipped to the shape of the shape layer.
Now, if you change the path that's installed in the shape layer to a rectangle that's the full size of the image, the system will animate the change for you.
As of March 21st 2019, it seems like the masksToBounds documentation is just wrong. masksToBounds does not seem to be animatable. I have tried animating masksToBounds with CABasicAnimation and CAKeyframeAnimations and it doesn't seem to work.

drawRect: How do I do an "inverted clip"

I am creating an iOS user interface to allow a user to pick a rectangle within an existing image, dragging the corners of that rectangle to the desired size. I now have four custom UIButtons (30% alpha) and a custom view (also with 30% alpha) that draws the dashed lines between the four corner buttons.
To "improve" the interface, I would like my drawRect code to make the cropped portion of the image appear "normal" while everything outside the cropped region is washed out (filled with white color, which will give me the correct effect since the UIView is set to 30% alpha).
The obvious algorithm would be:
Fill the entire image with [UIColor whiteColor] fill
Draw the four dashed lines with a [UIColor clearColor] fill
When I do this, the clear fill isn't showing up. I believe this is because the "fill" of the clear color in step #2 isn't being seen because the pixels were already set to white in step #1. Perhaps there's a blend mode that will allow me to see the transparency of the second rectangle? I'm not sure about the various blend modes.
My second attempt, which works, does the following:
Draw the four dashed lines with [UIColor clearColor] fill
Draw four additional rectangles with [UIColor whiteColor] fill, each representing the portions to the left, right, above, and below the cropped region.
As I mention, this method works, but seems to me there should be a simpler way instead of me having to calculate these four additional rectangles each and every time.
There is a similar question on SO Create layer mask with custom-shaped hole that uses CALayer and masks, but this seems to be overkill for what I need.
Does anybody have any suggestions on how to improve this?
You can set the blend mode to kCGBlendModeCopy and use clearColor to reset a pixel's alpha to zero. You can presumably also use kCGBlendModeClear but I haven't tested that.
You can also set the clipping path to just contain the pixels you want cleared and call CGContextClearRect(gc, CGRectInfinite).
If you want to use a clipping mask with a hole in it, you can do so without using a CALayer, and you can build it a little more simply than in the answer you linked, by using the even-odd rule and CGRectInfinite:
CGContextSaveGState(GC); {
CGContextBeginPath(gc);
CGContextAddRect(gc, myRect); // or whatever simple path you want here
CGContextAddRect(gc, CGRectInfinite);
CGContextEOClip(gc);
// drawing code here is clipped to the exterior of myRect
} CGContextRestoreGState(gc);

Resources