Add Implicit Animations to Your iOS Views, Part 1
iOS offers a wealth of animation routines with both UIKit and Core Animation. iOS 7 introduced new routines to provide keyframe animations and spring-like physics to extend the way you create fluid interfaces. In addition to these explicit routines, iOS offers several ways, relatively little known, to automatically animate view changes as you apply them. You add this implicit animation support with simple classes and just a few lines of code. When added, your views automatically animate between their before and after values without any further work on your part.
This is the first of a two-part write-up. In this post, you discover the ways you can add and control implicit animations with built-in properties. In the second part you can read about creating custom animatable properties.
Implicit Animations in Action
Video 1 introduces implicit animation. It shows an application that toggles the way its corners appear. In this simple app, updating the view layer's corner radius property creates an implicit animation sequence. Here is the entire method called by the go button in the video. It sets view's corner radius from 0 to 32 and back.
- (void) go { CGFloat newValue = (customView.layer.cornerRadius < 32) ? 32 : 0; customView.layer.cornerRadius = newValue; }
This method uses a standard property assignment, involving no animation timing, curves, or so forth. The implicit response lies in how the layer responds to property changes.
Video 1. When the view's corner radius updates, an implicit animation smoothly transitions from the old state to the new one.
Building an Animation-Ready View
The behavior you see in Video 1 is created by implementing the following CALayer subclass. Updates to the cornerRadius property invoke a basic animation. You override the actionForKey: method to add dynamic responses to property changes.
@interface CustomLayer : CALayer @property (nonatomic, assign) CGFloat animationDuration; @end @implementation CustomLayer // Return a basic animation - (CABasicAnimation *) customAnimationForKey: (NSString *) key { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key]; animation.fromValue = [self.presentationLayer valueForKey:key]; // Default to 0.3 second duration animation.duration = (_animationDuration == 0.0f) ? 0.3f : _animationDuration; return animation; } // Add a dynamic response for corner radius updates -(id<CAAction>)actionForKey:(NSString *)key { if ([key isEqualToString:@"cornerRadius"]) return [self customAnimationForKey:key]; return [super actionForKey:key]; } @end
The actionForKey method used in the preceding method is specific to the cornerRadius property. It won't animate changes to the layer's shadow, to its border width, or to any other elements. You update the method to animate all layer properties by removing that qualification, as in the following method.
// Add a dynamic response for all properties -(id<CAAction>)actionForKey:(NSString *)key { return [self customAnimationForKey:key]; } @end
Building a View Around the Layer
After creating a self-animating layer, you can incorporate it into a view. Create a UIView subclass and implement the layerClass class method, as in the following example. Instances of this subclass automatically use the CustomLayer layer class and animate their property changes.
@interface CustomView : UIView @end @implementation CustomView + (Class) layerClass { return [CustomLayer class]; } @end
Timing
Animations defined in the CustomLayer class do not respond to UIView animation calls, as you see in the following example. Consider the updated go method. It embeds its cornerRadius update into a view animation block.
- (void) go { CGFloat newValue = (customView.layer.cornerRadius < 32) ? 32 : 0; NSDate *date = [NSDate date]; [UIView animateWithDuration:2.0f animations:^{ customView.layer.cornerRadius = newValue; } completion:^(BOOL finished) { NSLog(@"Elapsed time: %f", [[NSDate date] timeIntervalSinceDate:date]); }]; }
Two things happen or more accurately do not happen here.
- First, the requested duration (2.0 seconds) is ignored. The animation lasts for the default time defined by the layer (0.3 seconds), not the 2 seconds set by the animation block.
- Second, the completion block runs almost immediately, not after a 2-second delay. As far as the UIView class is concerned, there are no animatable items to update, so the completion block executes at the end of the call.
Here are timing results from testing this a couple times. The elapsed time is close to zero.
2014-05-19 09:44:58.968 Hello World[60556:60b] Elapsed time: 0.007392 2014-05-19 09:44:59.529 Hello World[60556:60b] Elapsed time: 0.008787
Although there are ways to work around this by catching animation notifications and deriving the implicit animation duration and applying that to the layer, the approach is brittle and App Store unsafe. Instead, consider coordinating your animations instead.
Coordinating Animations
You coordinate implicit animations by adding them to a standard animation block along with explicit animations, as in the following example. Here, the layer's animation duration is manually set to match that of the animation block. By placing both updates in the same block, they occur simultaneously. The completion block executes after the explicit animation concludes.
- (void) go { // Retrieve the layer CustomLayer *customLayer = (CustomLayer *) customView.layer; // Match the animation duration customLayer.animationDuration = 2.0f; CGFloat newValue = (customView.layer.cornerRadius < 32) ? 32 : 0; NSDate *date = [NSDate date]; [UIView animateWithDuration:2.0f animations:^{ // Coordinate animations customLayer.cornerRadius = newValue; self.view.backgroundColor = _nextColor; } completion:^(BOOL finished) { NSLog(@"Elapsed time: %f", [[NSDate date] timeIntervalSinceDate:date]); }]; }
The completion block fires properly this time as there is a recognized UIView property to animate. The elapsed time moves to the expected 2 seconds.
2014-05-19 09:55:10.208 Hello World[60661:60b] Elapsed time: 2.000776 2014-05-19 09:55:13.562 Hello World[60661:60b] Elapsed time: 2.001607
Building Implicit Completion Blocks
You needn't rely on UIView calls to add completion blocks. You can build a custom completion block for your implicit animations with just a few tweaks. The following interface extends the CustomLayer class to add a completionBlock property.
typedef void (^ImplicitCompletionBlock)(NSString *key, BOOL finished); @interface CustomLayer : CALayer @property (nonatomic, assign) CGFloat animationDuration; @property (nonatomic, strong) ImplicitCompletionBlock completionBlock; @end
The custom ImplicitCompletionBlock type takes two arguments: a string corresponding to the animated property key path, for example "cornerRadius", and a flag that specifies whether the animation completed. The key enables you to distinguish between property updates in your block to better control any wrap-up.
The following methods implement the completion block details. The customAnimationForKey: method adds an animation delegate and stores the key with the animation. This enables the CustomLayer instance to catch the end of the animation and execute the optional block.
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { if (_completionBlock) { NSString *key = [anim valueForKey:@"Animation Type"]; _completionBlock(key, flag); } } - (CABasicAnimation *) customAnimationForKey: (NSString *) key { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key]; animation.fromValue = [self.presentationLayer valueForKey:key]; animation.duration = (_animationDuration == 0.0f) ? 0.3f : _animationDuration; animation.delegate = self; [animation setValue:key forKey:@"Animation Type"]; return animation; }
Wrap-Up
This write-up covered all the basics you need for adding simple intrinsic animations to your views. You can find the source code for the CustomLayer and CustomView classes at my Github repository. As you'll see, Part 2 of this series extends these classes to add custom properties beyond those natively found in CALayer instances.