Search code examples
iosuiimageviewcalayer

How to prevent clipping of UIImageView mask


I want to apply a chat bubble mask to an image view, and I would like the mask image to scale to fit in the frame of the image view. This is the code I am using to create and apply the mask:

let mask = CALayer()
let maskImage = UIImage(named: "chatGif")!
mask.contents = maskImage.CGImage
mask.frame = CGRectMake(0, 0, imageView.frame.width, imageView.frame.height)
imageView.layer.mask = mask
imageView.layer.masksToBounds = true

And this is my mask:

enter image description here

However this is what I get:

enter image description here

And when I add slicing to the mask image (i.e. create resizable cap insets) I get this:

enter image description here

I made a minimal project that demonstrates the issue and put it here

I do not fundamentally understand why the right and bottom sides of my mask image are getting clipped out of view. The mask's frame and bounds match the image view's, and the image used for the mask is actually smaller than the image view, so if anything I would expect it to not cover the whole thing, rather than getting partially clipped.

Can someone please explain what's happening here? How do I prevent this behavior?

EDIT: I've been playing around with this some more, and the results make a little more sense when I set the mask's size to match the maskImage size like so:

let mask = CALayer()
let maskImage = UIImage(named: "chatGif")!
mask.contents = maskImage.CGImage
mask.frame = CGRectMake(0, 0, maskImage.size.width, maskImage.size.height)
imageView.layer.mask = mask
imageView.layer.masksToBounds = true

This gives me:

enter image description here

Or with slicing on the mask asset, I get the image in J.Hunter's answer. This is still not what I want because the mask is not being stretched to fill the imageView and a large portion around the sides is masked out that shouldn't be, but I can at least understand what's happening in this case. I am setting the mask's frame to a size smaller than the imageView's frame so of course it doesn't fill the entire area.

However I would expect setting the frame of the mask to match that of the imageView would make the right and bottom edges of the mask that I can see in that last screenshot line up with the right and bottom edges of the imageView. If I print out the masks's frame, bounds, and contentsRect, they are exactly what I would expect: the frame and bounds match the imageView and the contentsRect is (0.0, 0.0, 1.0, 1.0). It was my understanding that a CALayer will display a portion of its contents based on the contentsRect, so unless that rect was smaller than 1.0x1.0, the entire contents image should be displayed. Right? But why is that not what I'm seeing?


Solution

  • Okay, I finally figured out what's going on. I added the same debug print statements to the sample project I made that I had put in my actual project and discovered that the imageViews frame at the time I was setting the mask was much larger that I expected it, and not its final display size. That makes sense because in the sample project I was setting the mask in viewDidLoad, before any subviews had been laid out.

    I didn't realize this was the case when printing out values in my actual project because my UIImageView is in a UITableViewCell, so it's un-initialized size was the same as the placeholder size in the storyboard, which wasn't that much larger than the final size, so it didn't read as obviously too big.

    Now if only I could get the last 4 hours of my life back.

    EDIT: To clarify, the initial code works fine if it's run after the image view has it's final layout size set. So you should set the mask in viewDidLayoutSubviews, or if it's a table/collection cell you can check if the frame size has changed in layoutSubviews.