Why do docs indicate CALayer animations must be in UIView animation blocks? - ios

I am currently reading Apple's Core Animation Guide, where I found the following passage regarding layer-backed views in iOS:
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.
Just below the quote, the documentation includes the following code listing:
[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"];
}];
which implies that both implicit and explicit animations on CALayers backing UIViews must be placed within an animation block.
However, I have found this to be patently untrue. Specifically, I have successfully implemented explicit animations using Core Animation classes outside of a UIView animation block.
Have I misunderstood this passage, or is it out-of-date, or something else?
Some additional notes:
I assume that "the UIView class disables layer animations by default but reenables them inside animation blocks" refers to the +[UIView setAnimationsEnabled:] method. When I get back to a computer that can do so, I'll check to see whether +[UIView areAnimationsEnabled] returns YES or NO.

That quote refers to the layer that is backing the view. It is not true for stand-alone layers that you create and manage yourself.
Every view on iOS is backed by a layer. When you change the view's properties, it changes the underlying layer property. By default the layer would have implicit animations, but the layer "disables" that behavior except for when you are inside of an UIView animation block. This is what that part of the documentation is referring to.
There are a couple of ways you can use Core Animation to animate a layer property. The most common is to add the animation object to the layer when you want to animate the property, but you can also make customizations through the actions dictionary and the styles dictionary if you always want to animate when a property changes. The last two would also be disabled for the layer that is backing a view.

Related

What is the default animation duration for UICollectionView layout changes?

Say I do this:
[self.collectionView setCollectionViewLayout:myNewLayout animated:YES];
What is the duration of the animation that happens?
My application of this is that I'm animating layer properties inside the cells when this changes, and because layer animations don't get caught by UIView's block-based animations, I have to do it separately with my own animation block.
It you know you're inside a UIView animation, then [CATransaction animationDuration] will give you what you need.
Swift
Use this:
CATransaction.animationDuration()
- Note:
You may need to check out and apply the timing curve too. Because it affects how the animation looks
Use this if so: CATransaction.animationTimingFunction()

How to synchronize CALayer and UIView animations up and down a complex hierarchy

See How to manage CALayer animations throughout a hierarchy for a follow-up question that is concerned just with how to synchronize parent-child layer animations.
This is a design question concerning running dependent animations at different levels of the view hierarchy in response to animations up the hierarchy.
I have a container controller that has any number of child controllers. The parent controller organizes this content on the screen and at various points needs to change the size and positions of its children views.
I'm trying to animate each of these child views to transition its size/shape/position from their original starting point to the destination. A basic first pass with some basic animations starts to get the job done.
Things get more complicated though by the fact that some of the views being resized should also be performing animations on the contents of the view. Imagine a child view with centered content. As the child is shrunk or expanded, the centered content should be animated alongside the outer animation to compensate for the bounds changes so that the content stays centered.
To further complicate matters, I also have a mask layer for each child view that needs to be animated alongside the child’s animation. I also have some gestures that require NO animations to be used for transitions - so I need a way to sometimes animate the whole view/layer tree, and sometimes not.
So all of this gives me an architecture where I have something like the following
ContainerViewController.view
-> auxiliary and decorative views
-> WrapperView (multiple)
----> mask layer
-> Child controller view
-> subviews & layers
Now my question is really one of maintainability. I can animate each of these parts using explicit or implicit animations. What I need to figure out is what’s the best way to make sure that all of the animations being done are done using the same duration and timing function. At present, I trigger a lot of these off of property changes. In some cases the property changes come from layoutSubviews (triggered from setNeedsLayout).
So, what’s the best strategy for setting up these animations, especially the explicit ones. Is the best that I can do just picking up values from CATransaction? My fear is that not every property needs to be animated in every case (like in the auxiliary views) - I already am flipping setDisableActions on/off to force/deny some property animations.
Should CATransaction be used to trigger the setup of explicit view animations? How do I bind the parameters specified for a UIView animation to the parameters that will be used for the underlying layers? The following code seems to get the job done, but seems really ugly.
-(void) animateForReason:(enum AnimationReason) animationReason
animations:(void(^)()) animationBlock completion:(void(^)(BOOL)) completionBlock {
const auto animationDuration = 3.0; // make this long to be noticeable!
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionLayoutSubviews
animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:animationDuration];
animationBlock();
[CATransaction commit];
}completion:completionBlock];
}
I think that UIViewControllerTransitionCoordinator is out because I need to do all of these animations in response to user actions, not just external things, like rotations or frame changes.
There are a few options to consider:
For UIView transitions, you can pass the UIViewAnimationOptionLayoutSubviews option. This will animate the changes between subviews before and after calling layoutSubviews on the view whose frame you just changed.
Instead of using layoutSubviews at all, you could override setBounds: or setFrame: on your UIView and CALayer subclasses. This way, if they're called within an animation block, the subviews will animate together with the superview. If they're not called within an animation block, they'll update instantly.
My fear is that not every property needs to be animated in every case (like in the auxiliary views) - I already am flipping setDisableActions on/off to force/deny some property animations.
Generally, if you want it animated, put it in an animation block, and if you don't, don't. Obviously, it can get more complex than this, which is why Apple sometimes has a setter with an animated argument (like setSelected:animated:).
If you have sometimes on, sometimes off properties, follow this pattern yourself. One possible implementation:
- (void) setNumberOfWidgets:(int)widgetCount animated:(BOOL)animated {
BOOL oldAnimationValue = [UIView areAnimationsEnabled];
if (!animated) {
[UIView setAnimationsEnabled:NO];
}
// a related property which may or may not cause an animation
self.someOtherProperty = someValue;
if (!animated) {
[UIView setAnimationsEnabled:oldAnimationValue];
}
}
My question is really one of maintainability.
Yes, it sounds like you're thinking about some kind of giant object that manages all the animation possibilities for you. Resist this temptation. Let each view be responsible for animating its own subviews, and tell a view (don't let it ask) whether or not these changes should be animated.
I think that UIViewControllerTransitionCoordinator is out because I need to do all of these animations in response to user actions, not just external things, like rotations or frame changes.
Yes, that's correct. Don't use UIViewControllerTransitionCoordinator for this.

Scope UIView animations to specific views or properties

I have a UIView with subviews and want to animate only specific properties of certain views. For example, I sometimes want to call [self layoutIfNeeded] and animate only the bounds but not other properties of the view or its subviews.
The problem is that +[UIView animateWithDuration:animations] tracks subviews and all animatable properties. Is there a reasonable solution to this?
Take a look at +[UIView performWithoutAnimation:]. You specify a block of changes you wish to perform without animation and they happen immediately.
This is good for iOS7 and above, and only for UIKit animation. For dealing with animations on layer objects directly, or support for older versions of iOS, you can use the following code:
[CATransaction begin];
[CATransaction setDisableActions:YES];
//Perform any changes that you do not want to be animated
[CATransaction commit];
More on performWithoutAnimation: here and on setDisabledActions: here.
If you do not wish to alter the parent's code, you can implement the setter methods of the properties you do not wish animated, and wrap the super call with performWithoutAnimation:, like so:
- (void)setFrame:(CGRect)frame
{
[UIView performWithoutAnimation: ^ {
[super setFrame:frame];
}];
}
If you don't want the subviews to resize when the parent view bounds change, you should set each of the subviews' autoresizingMask appropriately and/or set the parent's autoresizesSubviews flag.
Otherwise you have to override the parent's layoutSubviews and modify each of the subviews' frames back to their previous values.
Since you want to do something a bit more complex than just moving a view I would absolutely suggest you step down from UIKit to Core Animation. This will allow you to create animations on specific key paths for each object.
I promise it's not that scary, by creating a few CABasicAnimations and grouping them together in a CAAnimationGroup you can add them to any UIView's CALayer. This will definitely provide the control you need.

How to make UI object responsive after CABasicAnimation

I'm having trouble trying to find a way to make my UI object (UISegmentedControl) touch responsive after a CABasicAnimation slide in and bounce. How can I pull this off?
I know the UI object is in the presentation tree after the move. But I really like the setTimingFunction feature CABasicAnimation provides and I just won't be able to get such a smooth bounce using UIView animation.
Example GIF of animation (Looped):
Code I'm using inside viewDidLoad
CABasicAnimation *startAnimation = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
[startAnimation setFromValue:[NSNumber numberWithFloat:0]];
[startAnimation setToValue:[NSNumber numberWithFloat:slidingUpValue]];
[startAnimation setDuration:1.0];
startAnimation.fillMode = kCAFillModeForwards;
startAnimation.removedOnCompletion = NO;
[startAnimation setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.0 :0.0 :0.3 :1.8]];
[[gameTypeControl layer] addAnimation:startAnimation forKey:nil];
What went wrong
The problem is these two lines of code and not understanding the side effects of using them:
startAnimation.fillMode = kCAFillModeForwards;
startAnimation.removedOnCompletion = NO;
The first line configures the animation to keep showing the end value after the animation has completed (you can see a visualization of fillMode and the other timing related properties in my little cheat sheet).
The second line configures the animation to stay attached to the layer after it finishes.
This sounds just fine at first, but is missing an essential part of Core Animation: animations added to a layer never affect the model, only the presentation. The Core Animation Programming Guide mention this on the second page of the section "Core Animation Basics":
The data and state information of a layer object is decoupled from the visual presentation of that layer’s content onscreen.
Animations happen on a separate layer called the presentation layer which is what you see on screen. If you print out the values of the animated property during the animation they don't change at all.
During the animation you have a difference between the presentation and the model but the animation is probably short and that difference will go away as soon as the animation finishes, so it doesn't really matter... unless the animation doesn't go away. Then the difference has persisted and now you have to deal with having out-of-sync data in two places.
How not to solve this issue
One could say that everything looks good, it's just that hit testing is wrong. Let's patch hit testing! Change the hit testing to use the segment control's layer's presentation layer and hit testing will work. Great, right? This would be like putting one plaster on top of another instead of solving the problem at its origin.
How to get rid of the side effects
It's really simple: don't use those lines and remove animations when they are finished. One might say that this technique (not removing an animation) has been used by Apple in slides and sample code so it's what Apple recommends, but there is a subtle detail that is easily missed:
When Apple introduced Core Animation at WWDC 2007 they used this technique to animate layers being removed (for example, fading out). Quote below is from Session 211 - Adding Core Animation to Your Application:
To animate layer removal, use animations with
fillMode = forwards, removedOnCompletion = NO
Animation delegate can remove the layer
In this case it's perfectly fine to not remove the animation since it could cause the layer to jump back to it's original size, opacity, etc. for one frame before being removed from the layer hierarchy. As they said in the above slide: the "animation delegate can remove the layer" (that is: do the clean up).
In other cases no one does the clean up and you are left with the mess of having out of sync data in two places (as mentioned above).
The solution is a different approach to the animation
When building animations, I try to think of like this:
If your animation mysteriously went away, the model value should be the expected end state.
Applied to your example, the segmented control is moving towards it's final position to stop there and stay there. In that case the actual position of the segmented control should be the end position. The application should work even if the animation isn't there.
So what about the animation? Instead of animating from 0 offset to some offset, you reverse that and animate from some negative offset to 0 offset.
CABasicAnimation *startAnimation = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
[startAnimation setFromValue:#(-slidingUpValue)]]; // these two changed
[startAnimation setToValue:#(0.0)]; // places with each other
[startAnimation setDuration:1.0];
// no fill mode
// animation is removed on completion
[startAnimation setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.0 :0.0 :0.3 :1.8]];
[[gameTypeControl layer] addAnimation:startAnimation forKey:#"Move in game type control from bottom"]; // I like adding descriptive keys for easier debugging

iOS Core Animation Queue

I have multiple CALayers. And I want to be able to animate them sequentally using CAAnimation (and it's subclasses) animations. I wrote methods that prepare animations and add it to layer, one animation per method. Now, how I could create animaiton queue? CAAnimationGroup couldn't be used because animations are applied to different CALayers. NSOperationQueue isn't working because all UI actions should be done on main thread. The only solution I found is to create NSArray of NSInvocation objects, but this solution doesn't seem to work in iOS 5...
CAAnimationGroup couldn't be used because animations are applied to different CALayers
Actually, that's not true. A single CAAnimationGroup can animate different CALayers. You add the animation to the superlayer and refer to properties of the different layers using the key path notation "sublayers.[layername].[property]".
Use a common delegate for all your animations. As each animation ends, it will notify its delegate (-animationDidStop:finished:), which can then fire up the next one.

Resources