I have a CAShapeLayer (which is the layer of a UIView subclass) whose path
should update whenever the view's bounds
size changes. For this, I have overridden the layer's setBounds:
method to reset the path:
@interface CustomShapeLayer : CAShapeLayer
@end
@implementation CustomShapeLayer
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
self.path = [[self shapeForBounds:bounds] CGPath];
}
This works fine until I animate the bounds change. I would like to have the path animate alongside any animated bounds change (in a UIView animation block) so that the shape layer always adopts to its size.
Since the path
property does not animate by default, I came up with this:
Override the layer's addAnimation:forKey:
method. In it, figure out if a bounds animation is being added to the layer. If so, create a second explicit animation for animating the path alongside the bounds by copying all the properties from the bounds animation that gets passed to the method. In code:
- (void)addAnimation:(CAAnimation *)animation forKey:(NSString *)key
{
[super addAnimation:animation forKey:key];
if ([animation isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *basicAnimation = (CABasicAnimation *)animation;
if ([basicAnimation.keyPath isEqualToString:@"bounds.size"]) {
CABasicAnimation *pathAnimation = [basicAnimation copy];
pathAnimation.keyPath = @"path";
// The path property has not been updated to the new value yet
pathAnimation.fromValue = (id)self.path;
// Compute the new value for path
pathAnimation.toValue = (id)[[self shapeForBounds:self.bounds] CGPath];
[self addAnimation:pathAnimation forKey:@"path"];
}
}
}
I got this idea from reading David Rönnqvist's View-Layer Synergy article. (This code is for iOS 8. On iOS 7, it seems that you have to check the animation's keyPath
against @"bounds"
and not `@"bounds.size".)
The calling code that triggers the view's animated bounds change would look like this:
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
self.customShapeView.bounds = newBounds;
} completion:nil];
Questions
This mostly works, but I am having two problems with it:
Occasionally, I get a crash on (Edit: never mind. I forgot to convertEXC_BAD_ACCESS
) inCGPathApply()
when triggering this animation while a previous animation is still in progress. I am not sure whether this has anything to do with my particular implementation.UIBezierPath
toCGPathRef
. Thanks @antonioviero!When using a standard UIView spring animation, the animation of the view's bounds and the layer's path is slightly out of sync. That is, the path animation also performs in a springy way, but it does not follow the view's bounds exactly.
More generally, is this the best approach? It seems that having a shape layer whose path is dependent on its bounds size and which should animate in sync with any bounds changes is something that should just work™ but I'm having a hard time. I feel there must be a better way.
Other things I have tried
- Override the layer's
actionForKey:
or the view'sactionForLayer:forKey:
in order to return a custom animation object for thepath
property. I think this would be the preferred way but I did not find a way to get at the transaction properties that should be set by the enclosing animation block. Even if called from inside an animation block,[CATransaction animationDuration
] etc. always return the default values.
Is there a way to (a) determine that you are currently inside an animation block, and (b) to get the animation properties (duration, animation curve etc.) that have been set in that block?
Project
Here's the animation: The orange triangle is the path of the shape layer. The black outline is the frame of the view hosting the shape layer.
Have a look at the sample project on GitHub. (This project is for iOS 8 and requires Xcode 6 to run, sorry.)
Update
Jose Luis Piedrahita pointed me to this article by Nick Lockwood, which suggests the following approach:
Override the view's
actionForLayer:forKey:
or the layer'sactionForKey:
method and check if the key passed to this method is the one you want to animate (@"path"
).If so, calling
super
on one of the layer's "normal" animatable properties (such as@"bounds"
) will implicitly tell you if you are inside an animation block. If the view (the layer's delegate) returns a valid object (and notnil
orNSNull
), we are.Set the parameters (duration, timing function, etc.) for the path animation from the animation returned from
[super actionForKey:]
and return the path animation.
This does indeed work great under iOS 7.x. However, under iOS 8, the object returned from actionForLayer:forKey:
is not a standard (and documented) CA...Animation
but an instance of the private _UIViewAdditiveAnimationAction
class (an NSObject
subclass). Since the properties of this class are not documented, I can't use them easily to create the path animation.
_Edit: Or it might just work after all. As Nick mentioned in his article, some properties like backgroundColor
still return a standard CA...Animation
on iOS 8. I'll have to try it out.`