Use core animation on an Layer-Backed view - ios

In Core Animation Programming guide, there is one paragraph about How to Animate Layer-Backed Views, it says:
If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated.
There are also an example:
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
}];
In my opinion, it tells that if I don't issue Core Animation calls from inside a view-based animation block, there will no animation.
But it seems that if I add the core animation calls directly without view-based animation block, it works the same.
Have I missed something ?

tl;dr: The documentation only refers to implicit animations. Explicit animations work fine outside of animation blocks.
My paraphrasing of the documentation
The simplified version of that quote from the docs is something like (me paraphrasing it):
UIView have disabled implicit animations except for within animation blocks. If you want to do implicit layer animations you must do them inside an animation block.
What is implicit animations and how do they work?
Implicit animations is what happens when an animatable property of a standalone layer changes. For example, if you create a layer and change it's position it's going to animate to the new position. Many, many layer properties have this behaviour by default.
It happens something like this:
a transaction is started by the system (without us doing anything)
the value of a property is changed
the layer looks for the action for that property
at some point the transaction is committed (without us doing anything)
the action that was found is applied
Notice that there is no mention of animation above, instead there is the word "action". An action in this context refers to an object which implements the CAAction protocol. It's most likely going to be some CAAnimation subclass (like CABasicAnimation, CAKeyframeAnimation or CATransition) but is built to work with anything that conforms to that protocol.
How does it know what "action" to take?
Finding the action for that property happens by calling actionForKey: on the layer. The default implementation of this looks for an action in this order:
This search happens in this order (ref: actionForKey: documentation)
If the layer has a delegate and that delegate implements the Accessing the Layer’s Filters method, the layer calls that method. The delegate must do one of the following:
Return the action object for the given key.
Return nil if it does not handle the action.
Return the NSNull object if it does not handle the action and the search should be terminated.
The layer looks in the layer’s actions dictionary.
The layer looks in the style dictionary for an actions dictionary that contains the key.
The layer calls its defaultActionForKey: method to look for any class-defined actions.
The layer looks for any implicit actions defined by Core Animation.
What is UIView doing?
In the case of layers that are backing views, the view can enable or disable the actions by implementing the delegate method actionForLayer:forKey. For normal cases (outside an animation block) the view disables the implicit animations by returning [NSNull null] which means:
it does not handle the action and the search should be terminated.
However, inside the animation block, the view returns a real action. This can easily be verified by manually invoking actionForLayer:forKey: inside and outside the animation block. It could also have returned nil which would cause the layer to keep looking for an action, eventually ending up with the implicit actions (if any) if it wouldn't find anything before that.
When an action is found and the transaction is committed the action is added to the layer using the regular addAnimation:forKey: mechanism. This can easily be verified by creating a custom layer subclass and logging inside -actionForKey: and -addAnimation:forKey: and then a custom view subclass where you override +layerClass and return the custom layer class. You will see that the stand alone layer instance logs both methods for a regular property change but the backing layer does not add the animation, except when within a animation block.
Why this long explanation of implicit animations?
Now, why did I give this very long explanation of how implicit animations work? Well, it's to show that they use the same methods that you use yourself with explicit animations. Knowing how they work, we can understand what it means when the documentation say: "The UIView class disables layer animations by default but reenables them inside animation blocks".
The reason why explicit animations aren't disabled by what UIView does, is that you are doing all the work yourself: changing the property value, then calling addAnimation:forKey:.
The results in code:
Outside of animation block:
myView.backgroundColor = [UIColor redColor]; // will not animate :(
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // will not animate :(
[myView.layer addAnimation:myAnimation forKey:#"myKey"]; // animates :)
Inside of animation block:
myView.backgroundColor = [UIColor redColor]; // animates :)
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
[myView.layer addAnimation:myAnimation forKey:#"myKey"]; // animates :)
You can see above that explicit animations and implicit animations on standalone layers animate both outside and inside of animation blocks but implicit animations of layer-backed views does only animate inside the animation block.

I will explain it with a simple demonstration (example).
Add below code in your view controller.(don't forget to import QuartzCore)
#implementation ViewController{
UIView *view;
CALayer *layer;
}
- (void)viewDidLoad
{
[super viewDidLoad];
view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];
view.backgroundColor =[UIColor greenColor];
layer = [CALayer layer];
layer.frame =CGRectMake(0, 0, 50, 50);
layer.backgroundColor =[UIColor redColor].CGColor;
[view.layer addSublayer:layer];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
layer.frame =CGRectMake(70, 70, 50, 50);
}
See that in toucesBegan method there is no UIVIew animate block.
When you run the application and click on the screen, the opacity of the layer animates.This is because by default these are animatable.By default all the properties of a layer are animatable.
Consider now the case of layer backed views.
Change the code to below
- (void)viewDidLoad
{
[super viewDidLoad];
view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];
view.backgroundColor =[UIColor greenColor];
// layer = [CALayer layer];
// layer.frame =CGRectMake(0, 0, 50, 50);
// layer.backgroundColor =[UIColor redColor].CGColor;
// [view.layer addSublayer:layer];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
view.layer.opacity =0.0;
// layer.frame =CGRectMake(70, 70, 50, 50);
}
You might think that this will also animate its view.But it wont happen.Because layer backed views by default are not animatable.
To make those animations happen, you have to explicitly embed the code in UIView animate block.As shown below,
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:2.0 animations:^{
view.layer.opacity =0.0;
}];
// layer.frame =CGRectMake(70, 70, 50, 50);
}
That explains that,
The UIView class (which obviously was layer backed) disables layer animations by default but reenables them inside animation blocks.So any changes you make outside of an animation block are not animated.

Well, I've never used explicit Core Animation inside a view animation block. The documentation it seems to be not clear at all.
Probably the meaning is something like that, it's just a guess:
If you want to animate properties of the view that are linked to the
backed layer you should wrap them into a view animation block. In the
view's layer if you try to change the layer opacity this is not
animated, but if wrap into a view animation block it is.
In your snippet you are directly creating a basic animation thus explicitly creating an animation. Probably the doc just want to point out the differences between views and layers. In the latter animations on most properties are implicit.
You can see the difference if you write something like that:
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
}];
This will be animated.
myView.layer.opacity = 0.0;
This will not be animated.
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
This will be animated.

Related

How does the UIView.animate function internally work? [duplicate]

I'm new to Objective-C/iOS programming and I'm trying to understand how UIView animation works under the hood.
Say I have a code like this:
[UIView animateWithDuration:2.0 animations:^{
self.label.alpha = 1.0;
}];
The thing that gets passed as an animations argument is an Objective-C block (something like lambdas/anonymous functions in other languages) that can be executed and then it changes the alpha property of label from current value to 1.0.
However, the block does not accept an animation progress argument (say going from 0.0 to 1.0 or from 0 to 1000). My question is how the animation framework uses this block to know about intermediate frames, as the block only specifies the final state.
EDIT:
My questions is rather about under the hood operation of animateWithDuration method rather than the ways to use it.
My hypothesis of how animateWithDuration code works is as follows:
animateWithDuration activates some kind of special state for all view objects in which changes are not actually performed but only registered.
it executes the block and the changes are registered.
it queries the views objects for changed state and gets back the initial and target values and hence knows what properties to change and in what range to perform the changes.
it calculates the intermediate frames, based on the duration and initial/target values, and fires the animation.
Can somebody shed some light on whether animateWithDuration really works in such way?
Of course I don't know what exactly happens under the hood because UIKit isn't open-source and I don't work at Apple, but here are some ideas:
Before the block-based UIView animation methods were introduced, animating views looked like this, and those methods are actually still available:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:duration];
myView.center = CGPointMake(300, 300);
[UIView commitAnimations];
Knowing this, we could implement our own block-based animation method like this:
+ (void)my_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:duration];
animations();
[UIView commitAnimations];
}
...which would do exactly the same as the existing animateWithDuration:animations: method.
Taking the block out of the equation, it becomes clear that there has to be some sort of global animation state that UIView then uses to animate changes to its (animatable) properties when they're done within an animation block. This has to be some sort of stack, because you can have nested animation blocks.
The actual animation is performed by Core Animation, which works at the layer level – each UIView has a backing CALayer instance that is responsible for animations and compositing, while the view mostly just handles touch events and coordinate system conversions.
I won't go into detail here on how Core Animation works, you might want to read the Core Animation Programming Guide for that. Essentially, it's a system to animate changes in a layer tree, without explicitly calculating every keyframe (and it's actually fairly difficult to get intermediate values out of Core Animation, you usually just specify from and to values, durations, etc. and let the system take care of the details).
Because UIView is based on a CALayer, many of its properties are actually implemented in the underlying layer. For example, when you set or get view.center, that is the same as view.layer.location and changing either of these will also change the other.
Layers can be explicitly animated with CAAnimation (which is an abstract class that has a number of concrete implementations, like CABasicAnimation for simple things and CAKeyframeAnimation for more complex stuff).
So what might a UIView property setter do to accomplish "magically" animating changes within an animation block? Let's see if we can re-implement one of them, for simplicity's sake, let's use setCenter:.
First, here's a modified version of the my_animateWithDuration:animations: method from above that uses the global CATransaction, so that we can find out in our setCenter: method how long the animation is supposed to take:
- (void)my_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
animations();
[CATransaction commit];
}
Note that we don't use beginAnimations:... and commitAnimations anymore, so without doing anything else, nothing will be animated.
Now, let's override setCenter: in a UIView subclass:
#interface MyView : UIView
#end
#implementation MyView
- (void)setCenter:(CGPoint)position
{
if ([CATransaction animationDuration] > 0) {
CALayer *layer = self.layer;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [layer valueForKey:#"position"];
animation.toValue = [NSValue valueWithCGPoint:position];
layer.position = position;
[layer addAnimation:animation forKey:#"position"];
}
}
#end
Here, we set up an explicit animation using Core Animation that animates the underlying layer's location property. The animation's duration will automatically be taken from the CATransaction. Let's try it out:
MyView *myView = [[MyView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
myView.backgroundColor = [UIColor redColor];
[self.view addSubview:myView];
[self my_animateWithDuration:4.0 animations:^{
NSLog(#"center before: %#", NSStringFromCGPoint(myView.center));
myView.center = CGPointMake(300, 300);
NSLog(#"center after : %#", NSStringFromCGPoint(myView.center));
}];
I'm not saying that this is exactly how the UIView animation system works, it's just to show how it could work in principle.
The values intermediate frames for are not specified; the animation of the values (alpha in this case, but also colours, position, etc) is generated automatically between the previously set value and the destination value set inside the animation block. You can affect the curve by specifying the options using animateWithDuration:delay:options:animations:completion: (the default is UIViewAnimationOptionCurveEaseInOut, i.e., the speed of the value change will accelerate and decelerate).
Note that any previously set animated changes of values will finish first, i.e., each animation block specifies a new animation from the previous end value to the new. You can specify UIViewAnimationOptionBeginFromCurrentState to start the new animation from the state of the already in-progress animation.
This will change the property specified inside the block from the current value to whatever value you provide, and will do it linearly over the duration.
So if the original alpha value was 0, this would fade in the label over 2 seconds. If the original values was already 1.0, you wouldn't see any effect at all.
Under the hood, UIView takes care of figuring out over how many animation frames the change needs to take place.
You can also change the rate at which the change takes place by specifying an easing curve as a UIViewAnimationOption. Again, UIView handles the tweening for you.

UIView's Backing CALayer Does Not Reenable Implicit Animations In Block Anymore?

According to official documentation,
The UIView class disables layer animations by default but reenables them inside animation blocks.
backing layer's implicit animations should be reenables within the UIView's animation blocks. In fact, this official snippets
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
}];
attempts to trigger implicit animations on the backing layer. Also, according the the answer from this question, this is the expected behavior. This blog's post also confirmed that
the view returns the NSNull object outside of the block and returns a CABasicAnimation inside of the block
However, this is currently not true. From my simple test, UIView's animation block does not reenable the implicit animations of back CALayer. In addition, actionForLayer:forKey: now return NSNull both inside and outside of the animation block. Does this means the official is out-dated and this is no longer the expected behavior?

Can't get a CALayer to update its drawLayer: DURING a bounds animation

I'm trying to animate a custom UIView's bounds while also keeping its layer the same size as its parent view. To do that, I'm trying to animate the layers bounds alongside its parent view. I need the layer to call drawLayer:withContext AS its animating so my custom drawing will change size correctly along with the bounds.
drawLayer is called correctly and draws correctly before I start the animation. But I can't get the layer to call its drawLayer method on EACH step of the bounds animation. Instead, it just calls it ONCE, jumping immediately to the "end bounds" at the final frame of the animation.
// self.bg is a property pointing to my custom UIView
self.bg.layer.needsDisplayOnBoundsChange = YES;
self.bg.layer.mask.needsDisplayOnBoundsChange = YES;
[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
[CATransaction begin];
self.bg.layer.bounds = bounds;
self.bg.layer.mask.bounds = bounds;
[CATransaction commit];
self.bg.bounds = bounds;
} completion:nil];
Why doesn't the bounds report a change AS its animating (not just the final frame)? What am I doing wrong?
This might or might not help...
Many people are unaware that Core Animation has a supercool feature that allows you to define your own layer properties in such a way that they can be animated. An example I use is to give a CALayer subclass a thickness property. When I animate it with Core Animation...
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"thickness"];
ba.toValue = #10.0f;
ba.autoreverses = YES;
[lay addAnimation:ba forKey:nil];
...the effect is that drawInContext: or drawLayer:... is called repeatedly throughout the animation, allowing me to change repeatedly the way the layer is drawn in accordance with its current thickness property value at each moment (an intermediate value in the course of the animation).
It seems to me that that might be the sort of thing you're after. If so, you can find a working downloadable example here:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch17p498customAnimatableProperty
Discussion (from my book) here:
http://www.apeth.com/iOSBook/ch17.html#_making_a_property_animatable
This is because the layer you are drawing to is not the same layer as the one displayed on the screen.
When you animate a layer property it will immediately be set to its final value in the model layer, (as you have noticed), and the actual animation is done in the presentation layer.
You can access the presentation layer and see the actual values of the animated properties:
CALayer *presentationLayer = (CALayer *)[self.bg.layer presentationLayer];
...
Since you haven't provided your drawLayer:withContext method, it's unclear what you want to draw during the animation, but if you want to animate custom properties, here is a good tutorial for doing that.
Firstly, the layer of a layer backed (or hosting) view is always resized to fit the bounds of its parent view. If you set the view to be the layers delegate then the view will receive drawLayer:inContext: at each frame. Of course you must ensure that If your layer has needsDisplayOnBoundsChange == YES.
Here is an example (on the Mac) of resizing a window, which then changes the path of the underlying layer.
// My Nib contains one view and one button.
// The view has a MPView class and the button action is resizeWindow:
#interface MPView() {
CAShapeLayer *_hostLayer;
CALayer *_outerLayer;
CAShapeLayer *_innerLayer;
}
#end
#implementation MPView
- (void)awakeFromNib
{
[CATransaction begin];
[CATransaction setDisableActions:YES];
_hostLayer = [CAShapeLayer layer];
_hostLayer.backgroundColor = [NSColor blackColor].CGColor;
_hostLayer.borderColor = [NSColor redColor].CGColor;
_hostLayer.borderWidth = 2;
_hostLayer.needsDisplayOnBoundsChange = YES;
_hostLayer.delegate = self;
_hostLayer.lineWidth = 4;
_hostLayer.strokeColor = [NSColor greenColor].CGColor;
_hostLayer.needsDisplayOnBoundsChange = YES;
self.layer = _hostLayer;
self.wantsLayer = YES;
[CATransaction commit];
[self.window setFrame:CGRectMake(100, 100, 200, 200) display:YES animate:NO];
}
- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
if (layer == _hostLayer) {
CGSize size = layer.bounds.size;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathAddLineToPoint(path, NULL, size.width, size.height);
_hostLayer.path = path;
CGPathRelease(path);
}
}
- (IBAction)resizeWindow:(id)sender
{
[self.window setFrame:CGRectMake(100, 100, 1200, 800) display:YES animate:YES];
}
#end

How do I create a smoothly resizable circular UIView?

I'm trying to create a UIView which shows a semitransparent circle with an opaque border inside its bounds. I want to be able to change the bounds in two ways - inside a -[UIView animateWithDuration:animations:] block and in a pinch gesture recogniser action which fires several times a second. I've tried three approaches based on answers elsewhere on SO, and none are suitable.
Setting the corner radius of the view's layer in layoutSubviews gives smooth translations, but the view doesn't stay circular during animations; it seems that cornerRadius isn't animatable.
Drawing the circle in drawRect: gives a consistently circular view, but if the circle gets too big then resizing in the pinch gesture gets choppy because the device is spending too much time redrawing the circle.
Adding a CAShapeLayer and setting its path property in layoutSublayersOfLayer, which doesn't animate inside UIView animations since path isn't implicitly animatable.
Is there a way for me to create a view which is consistently circular and smoothly resizable? Is there some other type of layer I could use to take advantage of the hardware acceleration?
UPDATE
A commenter has asked me to expand on what I mean when I say that I want to change the bounds inside a -[UIView animateWithDuration:animations:] block. In my code, I have a view which contains my circle view. The circle view (the version that uses cornerRadius) overrides -[setBounds:] in order to set the corner radius:
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = fminf(bounds.size.width, bounds.size.height) / 2.0;
[super setBounds:bounds];
}
The bounds of the circle view are set in -[layoutSubviews]:
-(void)layoutSubviews
{
// some other layout is performed and circleRadius and circleCenter are
// calculated based on the properties and current size of the view.
self.circleView.bounds = CGRectMake(0, 0, circleRadius*2, circleRadius*2);
self.circleView.center = circleCenter;
}
The view is sometimes resized in animations, like so:
[UIView animateWithDuration:0.33 animations:^(void) {
myView.frame = CGRectMake(x, y, w, h);
[myView setNeedsLayout];
[myView layoutIfNeeded];
}];
but during these animations, if I draw the circle view using a layer with a cornerRadius, it goes funny shapes. I can't pass the animation duration in to layoutSubviews so I need to add the right animation within -[setBounds].
As the section on Animations in the "View Programming Guide for iOS" says
Both UIKit and Core Animation provide support for animations, but the level of support provided by each technology varies. In UIKit, animations are performed using UIView objects
The full list of properties that you can animate using either the older
[UIView beginAnimations:context:];
[UIView setAnimationDuration:];
// Change properties here...
[UIView commitAnimations];
or the newer
[UIView animateWithDuration:animations:];
(that you are using) are:
frame
bounds
center
transform (CGAffineTransform, not the CATransform3D)
alpha
backgroundColor
contentStretch
What confuses people is that you can also animate the same properties on the layer inside the UIView animation block, i.e. the frame, bounds, position, opacity, backgroundColor.
The same section goes on to say:
In places where you want to perform more sophisticated animations, or animations not supported by the UIView class, you can use Core Animation and the view’s underlying layer to create the animation. Because view and layer objects are intricately linked together, changes to a view’s layer affect the view itself.
A few lines down you can read the list of Core Animation animatable properties where you see this one:
The layer’s border (including whether the layer’s corners are rounded)
There are at least two good options for achieving the effect that you are after:
Animating the corner radius
Using a CAShapeLayer and animating the path
Both of these require that you do the animations with Core Animation. You can create a CAAnimationGroup and add an array of animations to it if you need multiple animations to run as one.
Update:
Fixing things with as few code changes as possible would be done by doing the corner radius animation on the layer at the "same time" as the other animations. I put quotations marks around same time since it is not guaranteed that animations that are not in the same group will finish at exactly the same time. Depending on what other animations you are doing it might be better to use only basic animations and animations groups. If you are applying changes to many different views in the same view animation block then maybe you could look into CATransactions.
The below code animates the frame and corner radius much like you describe.
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)];
[[circle layer] setCornerRadius:50];
[[circle layer] setBorderColor:[[UIColor orangeColor] CGColor]];
[[circle layer] setBorderWidth:2.0];
[[circle layer] setBackgroundColor:[[[UIColor orangeColor] colorWithAlphaComponent:0.5] CGColor]];
[[self view] addSubview:circle];
CGFloat animationDuration = 4.0; // Your duration
CGFloat animationDelay = 3.0; // Your delay (if any)
CABasicAnimation *cornerRadiusAnimation = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
[cornerRadiusAnimation setFromValue:[NSNumber numberWithFloat:50.0]]; // The current value
[cornerRadiusAnimation setToValue:[NSNumber numberWithFloat:10.0]]; // The new value
[cornerRadiusAnimation setDuration:animationDuration];
[cornerRadiusAnimation setBeginTime:CACurrentMediaTime() + animationDelay];
// If your UIView animation uses a timing funcition then your basic animation needs the same one
[cornerRadiusAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
// This will keep make the animation look as the "from" and "to" values before and after the animation
[cornerRadiusAnimation setFillMode:kCAFillModeBoth];
[[circle layer] addAnimation:cornerRadiusAnimation forKey:#"keepAsCircle"];
[[circle layer] setCornerRadius:10.0]; // Core Animation doesn't change the real value so we have to.
[UIView animateWithDuration:animationDuration
delay:animationDelay
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
[[circle layer] setFrame:CGRectMake(50, 50, 20, 20)]; // Arbitrary frame ...
// You other UIView animations in here...
} completion:^(BOOL finished) {
// Maybe you have your completion in here...
}];
With many thanks to David, this is the solution I found. In the end what turned out to be the key to it was using the view's -[actionForLayer:forKey:] method, since that's used inside UIView blocks instead of whatever the layer's -[actionForKey] returns.
#implementation SGBRoundView
-(CGFloat)radiusForBounds:(CGRect)bounds
{
return fminf(bounds.size.width, bounds.size.height) / 2;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
self.layer.backgroundColor = [[UIColor purpleColor] CGColor];
self.layer.borderColor = [[UIColor greenColor] CGColor];
self.layer.borderWidth = 3;
self.layer.cornerRadius = [self radiusForBounds:self.bounds];
}
return self;
}
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = [self radiusForBounds:bounds];
[super setBounds:bounds];
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if ([event isEqualToString:#"cornerRadius"])
{
CABasicAnimation *boundsAction = (CABasicAnimation *)[self actionForLayer:layer forKey:#"bounds"];
if ([boundsAction isKindOfClass:[CABasicAnimation class]] && [boundsAction.fromValue isKindOfClass:[NSValue class]])
{
CABasicAnimation *cornerRadiusAction = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
cornerRadiusAction.delegate = boundsAction.delegate;
cornerRadiusAction.duration = boundsAction.duration;
cornerRadiusAction.fillMode = boundsAction.fillMode;
cornerRadiusAction.timingFunction = boundsAction.timingFunction;
CGRect fromBounds = [(NSValue *)boundsAction.fromValue CGRectValue];
CGFloat fromRadius = [self radiusForBounds:fromBounds];
cornerRadiusAction.fromValue = [NSNumber numberWithFloat:fromRadius];
return cornerRadiusAction;
}
}
return action;
}
#end
By using the action that the view provides for the bounds, I was able to get the right duration, fill mode and timing function, and most importantly delegate - without that, the completion block of UIView animations didn't run.
The radius animation follows that of the bounds in almost all circumstances - there are a few edge cases that I'm trying to iron out, but it's basically there. It's also worth mentioning that the pinch gestures are still sometimes jerky - I guess even the accelerated drawing is still costly.
Starting in iOS 11, UIKit animates cornerRadius if you change it inside an animation block.
The path property of a CAShapeLayer isn't implicitly animatable, but it is animatable. It should be pretty easy to create a CABasicAnimation that changes the size of the circle path. Just makes sure that the path has the same number of control points (e.g. changing the radius of a full-circle arc.) If you change the number of control points, things get really strange. "Results are undefined", according to the documentaiton.

CAGradientLayer properties not animating within UIView animation block

I have a feeling I'm overlooking something elementary, but what better way to find it than to be wrong on the internet?
I have a fairly basic UI. The view for my UIViewController is a subclass whose +layerClass is CAGradientLayer. Depending on the user's actions, I need to move some UI elements around, and change the values of the background's gradient. The code looks something like this:
[UIView animateWithDuration:0.3 animations:^{
self.subview1.frame = CGRectMake(...);
self.subview2.frame = CGRectMake(...);
self.subview2.alpha = 0;
NSArray* newColors = [NSArray arrayWithObjects:
(id)firstColor.CGColor,
(id)secondColor.CGColor,
nil];
[(CAGradientLayer *)self.layer setColors:newColors];
}];
The issue is that the changes I make in this block to the subviews animate just fine (stuff moves and fades), but the change to the gradient's colors does not. It just swaps.
Now, the documentation does say that Core Animation code within an animation block won't inherit the block's properties (duration, easing, etc.). But is it the case that that doesn't define an animation transaction at all? (The implication of the docs seems to be that you'll get a default animation, where I get none.)
Do I have to use explicit CAAnimation to make this work? (And if so, why?)
There seem to be two things going on here. The first (as Travis correctly points out, and the documentation states) is that UIKit animations don't seem to hold any sway over the implicit animation applied to CALayer property changes. I think this is weird (UIKit must be using Core Animation), but it is what it is.
Here's a (possibly very dumb?) workaround for that problem:
NSTimeInterval duration = 2.0; // slow things down for ease of debugging
[UIView animateWithDuration:duration animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
// ... do stuff to things here ...
[CATransaction commit];
}];
The other key is that this gradient layer is my view's layer. That means that my view is the layer's delegate (where, if the gradient layer was just a sublayer, it wouldn't have a delegate). And the UIView implementation of -actionForLayer:forKey: returns NSNull for the "colors" event. (Probably every event that isn't on a specific list of UIView animations.)
Adding the following code to my view will cause the color change to be animated as expected:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if( [#"colors" isEqualToString:event]
&& (nil == action || (id)[NSNull null] == action) ) {
action = [CABasicAnimation animationWithKeyPath:event];
}
return action;
}
You have to use explicit CAAnimations, because you're changing the value of a CALayer.
UIViewAnimations work on UIView properties, but not directly on their CALayer's properties...
Actually, you should use a CABasicAnimation so that you can access its fromValue and toValue properties.
The following code should work for you:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[UIView animateWithDuration:2.0f
delay:0.0f
options:UIViewAnimationCurveEaseInOut
animations:^{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"colors"];
animation.duration = 2.0f;
animation.delegate = self;
animation.fromValue = ((CAGradientLayer *)self.layer).colors;
animation.toValue = [NSArray arrayWithObjects:(id)[UIColor blackColor].CGColor,(id)[UIColor whiteColor].CGColor,nil];
[self.layer addAnimation:animation forKey:#"animateColors"];
}
completion:nil];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
NSString *keyPath = ((CAPropertyAnimation *)anim).keyPath;
if ([keyPath isEqualToString:#"colors"]) {
((CAGradientLayer *)self.layer).colors = ((CABasicAnimation *)anim).toValue;
}
}
There is a trick with CAAnimations in that you HAVE to explicitly set the value of the property AFTER you complete the animation.
You do this by setting the delegate, in this case I set it to the object which calls the animation, and then override its animationDidStop:finished: method to include the setting of the CAGradientLayer's colors to their final value.
You'll also have to do a bit of casting in the animationDidStop: method, to access the properties of the animation.

Resources