Add Implicit Animations to Your iOS Views, Part 2
Implicit animations create visual changes that update in tandem with view properties. You add this support by implementing a few simple classes . When implemented, your views automatically animate between before and after values without any further work on your part.
In the first part of this write-up, you read about animating built-in properties. Now it's time to move forward to custom ones. If you can connect a visual representation to an inherent instance value and draw that value in some way, you can build an animatable property. This write-up discusses how.
Custom Layer Properties
A custom property is as wild or as limited as your imagination. Video 1 showcases a simple logo that fades into view and then out again. As with the properties in the first part of this write-up, this animation is implicit. The animation is applied as a view layer's property changes . In this case an NSNumber property called logoLevel triggers the animation. The property ranges from 0.0 (fully transparent) to 1.0 (fully opaque). The layer smoothly adjusts its presentation, no matter what value you set.
Video 1. The carrot logo animates into and out of view as the layer's logoLevel property updates.
Custom animations coexist happily with their built-in fellows. Video 2 shows the logo animation combined with shadow and border width changes. It achieves this without any ill effect or interference between the properties.
Video 2. The logo updates do not interfere with the built-in updates.
The following method enables the user to trigger the animations you see in this video. Every time the user taps the Go button, each layer property is assigned a new value.
- (void) go { static BOOL visible = NO; visible = !visible; CustomLayer *customLayer = (CustomLayer *) customView.layer; customLayer.logoLevel = visible ? @(1.0) : @(0.0); customLayer.borderWidth = visible ? 8 : 4; customLayer.shadowOpacity = visible ? 0.5 : 0; }
Creating Custom Dynamic Properties
Here's how you build the fading logo effect you saw in the preceding videos. Start by creating a custom property. In this example, the logo level property stores an NSNumber instance.
@interface CustomLayer : CALayer @property (nonatomic, assign) CGFloat animationDuration; @property (nonatomic, strong) NSNumber *logoLevel; @end
Declare this property as @dynamic in the implementation. This enables the layer class to dynamically implement the accessor methods for this custom property at run time. When the property value updates, the layer will be ready to handle those changes.
@implementation CustomLayer @dynamic logoLevel; /class implementation/ @end
Intercepting Updates
To catch properties whose updates require animated changes, implement the needsDisplayForKey: method, as in the following code snippet. The key value coding used here means you compare the supplied key to the name of the custom property.
+ (BOOL) needsDisplayForKey:(NSString *) key { if ([key isEqualToString:@"logoLevel"]) return YES; return [super needsDisplayForKey:key]; }
You implement this method in CALayer subclasses. Returning YES enables you to mark those properties whose contents need to be redrawn when their value changes. Defer to the superclass's implementation for any property you don't handle.
Drawing Properties
The magic behind custom property animation lies in implementing drawInContext: . This method is superficially similar to the drawRect: method used in UIView subclasses. As with drawRect:, you implement a custom presentation by drawing your changes into a context. Here's an example that shows the relationship between the custom logoLevel property and the drawing it produces. Dividing the logoLevel value by two ensures the logo is draw at a maximum alpha level of 0.5.
- (void) drawInContext:(CGContextRef) context { UIGraphicsPushContext(context); UIBezierPath *path = [self path]; CGFloat alpha = self.logoLevel.floatValue / 2.0f; // range from 0.0 to 0.5 [path fill:[[UIColor whiteColor] colorWithAlphaComponent:alpha]]; UIGraphicsPopContext(); }
This method is called repeatedly as the layer interpolates between its old and new values. Each time it's called, the new alpha level creates a different fill result.
You aren't, of course, limited to a single animatable property. In the following method, both logoLevel and imageLevel properties control the drawing produced by the layer subclass. The imageLevel property allows the image to fade in and out, just as the logo drawing does, but without having to do so in tandem.
- (void) drawInContext:(CGContextRef) context { UIGraphicsPushContext(context); // Draw the path UIBezierPath *path = [self path]; CGFloat alpha = self.logoLevel.floatValue / 2.0f; [path fill:[[UIColor whiteColor] colorWithAlphaComponent:alpha]]; // Draw an image [image drawInRect:CGRectMake(20, 20, 64, 64) blendMode:kCGBlendModeCopy alpha:self.imageLevel.floatValue]; UIGraphicsPopContext(); }
As a rule of thumb, keep your drawing simple and local using resources set as instance variables within the layer rather than adjusted by outside properties. When in doubt, slow down your animation. Properly behaved items update gradually.
Good candidates for custom intrinsic animation include any view that updates to reflect state. For example, you might indicate items selected by the user by applying check marks, thickening frames, or changing the opacity of an overlay. The best applications though are ones with nuance. Instead of switching a display feature on or off, adjust it continuously within a range of values. The implicit animations ensure that your visual properties update and display smoothly.
Wrap-Up
This write-up covered all the know-how you need to add implicit animations for custom properties to your views. As with the previous write-up, you'll find the associated source code at my Github repository.