Animating a CALayer's mask size change

I found the solution to this problem. Other answers are partially correct and are helpful.

The following points are important to understanding the solution:

  • The mask property is not animatable itself.
  • Since the mask is a CALayer it can be animated on its own.
  • Frame is not animatable, use bounds and position. This may not apply to you(if you weren't trying to animate the frame), but was an issue for me. (See Apple QA 1620)
  • A view layer's mask is not tied to UIView so it will not receive the core animation transaction that is applied to the view's layer.
  • We are modifying the CALayer directly, so we can't expect that UIView will have any idea of what we are trying to do, so the UIView animation won't create the core animation transaction to include changes to our properties.

In order to solve, we are going to have to tap into Core Animation ourselves, and can't rely on the UIView animation block to do the work for us.

Simply create a CATransaction with the same duration that you are using with [UIView animateWithDuration:...]. This will create a separate animation, but if your durations and easing function is the same, it should animate exactly with the other animations in your animation block.

NSTimeInterval duration = 0.5;// match this to the value of the UIView animateWithDuration: call

[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:duration] forKey:kCATransactionAnimationDuration];

self.myView.layer.mask.position = CGPointMake(newX, 0);
self.myView.layer.mask.bounds = CGRectMake(0, 0, newWidth, newHeight);

[CATransaction commit];

I use a CAShapeLayer to mask a UIView by setting self.layer.mask to that shape layer.

To animate the mask whenever the size of the view changes I overwrote the -setBounds: to animate the mask layer path if the bounds are changed during an animation.

Here's how I implemented it:

- (void)setBounds:(CGRect)bounds
{
    [super setBounds:bounds];
    CAPropertyAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds"];

    // update the mask
    self.maskLayer.frame = self.layer.bounds;

    // if the bounds change happens within an animation, also animate the mask path
    if (!boundsAnimation) {
        self.maskLayer.path = [self createMaskPath];
    } else {
        // copying the original animation allows us to keep all animation settings
        CABasicAnimation *animation = [boundsAnimation copy];
        animation.keyPath = @"path";

        CGPathRef newPath = [self createMaskPath];
        animation.fromValue = (id)self.maskLayer.path;
        animation.toValue = (__bridge id)newPath;

        self.maskLayer.path = newPath;

        [self.maskLayer addAnimation:animation forKey:@"path"];
    }
}

(For the example self.maskLayer is set to `self.layer.mask)

My -createMaskPath calculates the CGPathRef that I use to mask the view. I also update the mask path in -layoutSubviews.


The mask property of CALayer is not animatable which explains your lack of luck in that direction.

Does the drawing of your mask depend on the frame/bounds of the mask? (Can you provide some code?) Does the mask have needsDisplayOnBoundsChange property set?

Cheers, Corin