Chapter 9: Layers Like an Onion: Core Animation
The iPhone has made animation central to the mobile experience. Views slide in and out, applications zoom into place, pages fly into the bookmark list. Apple has made animation not just a beautiful part of the experience, but a better way to let the user know what’s happening and what to expect. When views slide into place from right to left, it’s natural to press the left-pointing button to go back to where you were. When you create a bookmark and it flies to the toolbar, it’s obvious where you should look to get back to that bookmark. These subtle cues are a critical part of making your user interface intuitive as well as engaging. To facilitate all this animation, iOS devices include a powerful GPU and frameworks that let you harness that GPU easily.
In this chapter, you discover the two main animation systems of iOS: view animations and the Core Animation framework. You find out how to draw with Core Animation layers and how to move layers around in two and three dimensions. Common decorations like rounded corners, colored borders, and shadows are trivial with CALayer
, and you discover how to apply them quickly and easily. You learn how to create custom automatic animations, including animating your own properties. Finally, Core Animation is all about performance, so you find out how to manage layers in multithreaded applications.
This chapter focuses on animations for view-based programming. These frameworks are ideal for most iOS applications except games. Game development is outside the scope of this book, and it’s usually best served by built-in frameworks like OpenGL ES or third-party frameworks like Cocos2D. For more information on OpenGL ES, see the OpenGL ES for iOS portal at developer.apple.com
. For more information on Cocos2D, see cocos2d-iphone.org
.
View Animations
UIView
provides rich animation functionality that’s very easy to use and well optimized. Most common animations can be handled with +animateWithDuration:animations:
and related methods. You can use UIView
to animate frame
, bounds
, center
, transform
, alpha
, backgroundColor
, and contentStretch
. Most of the time, you’ll animate frame
, center
, transform
, and alpha
.
It’s likely that you’re familiar with basic view animations, so I’ll just touch on the high points in this section and then move on to more advanced layer-based drawing and animation.
Let’s start with a very simple animation of a ball that falls when you tap the view. CircleView
just draws a circle in its frame. The following code creates the animation shown in Figure 9-1.
ViewAnimationViewController.m (ViewAnimation)
- (void)viewDidLoad {
[super viewDidLoad];
self.circleView = [[CircleView alloc] initWithFrame:
CGRectMake(0, 0, 20, 20)];
self.circleView.center = CGPointMake(100, 20);
[[self view] addSubview:self.circleView];
UITapGestureRecognizer *g;
g = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(dropAnimate)];
[[self view] addGestureRecognizer:g];
}
...
- (void)dropAnimate {
[UIView animateWithDuration:3 animations:^{
self.circleView.center = CGPointMake(100, 300);
}];
}
CircleView animation
This is the simplest kind of view-based animation, and it can handle most common problems, particularly animating size, location, and opacity. It’s also common to animate transform
to scale, rotate, or translate the view over time. Less commonly, you can animate backgroundColor
and contentStretch
. Animating the background color is particularly useful in HUD-style interfaces to move between mostly transparent and mostly opaque backgrounds. This can be more effective than just animating the overall alpha
.
Chaining animations is also straightforward, as shown in the following code.
- (void)dropAnimate {
[UIView
animateWithDuration:3 animations:^{
self.circleView.center = CGPointMake(100, 300);
}
completion:^(BOOL finished){
[UIView animateWithDuration:1 animations:^{
self.circleView.center = CGPointMake(250, 300);
}
];
}];
}
Now the ball will drop and then move to the right. But there’s a subtle problem with this code. If you tap the screen while the animation is in progress, the ball will jump to the lower-left and then animate to the right. That’s probably not what you want. The issue is that every time you tap the view, this code runs. If an animation is in progress, then it’s canceled and the completion
block runs with finished==NO
. You look at how to handle that next.
Managing User Interaction
The problem mentioned in the previous section is caused by a user experience mistake: allowing the user to send new commands while you’re animating the last command. Sometimes that’s what you want, but in this case, it isn’t. Anytime you create an animation in response to user input, you need to consider this issue.
When you animate a view, by default it automatically stops responding to user interaction. So, while the ball is dropping, tapping it won’t generate any events. In this example, however, tapping the main view causes the animation. There are two solutions. First, you can change your user interface so that tapping the ball causes the animation:
[self.circleView addGestureRecognizer:g];
The other solution is to ignore taps while the ball is animating. The following code shows how to disable the UIGestureRecognizer
in the gesture recognizer callback and then enable it when the animation completes.
- (void)dropAnimate:(UIGestureRecognizer *)recognizer {
[UIView
animateWithDuration:3 animations:^{
recognizer.enabled = NO;
self.circleView.center = CGPointMake(100, 300);
}
completion:^(BOOL finished){
[UIView
animateWithDuration:1 animations:^{
self.circleView.center = CGPointMake(250, 300);
}
completion:^(BOOL finished){
recognizer.enabled = YES;
}];
}];
}
This technique is nice because it minimizes side effects to the rest of the view, but you might want to prevent all user interaction for the view while the animation runs. In that case, you replace recognizer.enabled
with self.view.userInteractionEnabled
.
Drawing with Layers
View animations are powerful, so rely on them whenever you can, especially for basic layout animation. They also provide a small number of stock transitions that you can read about in the Animations section of the View Programming Guide for iOS available at developer.apple.com
. If you have basic needs, these are great tools.
But you’re here to go beyond the basic needs, and view animations have a lot of limitations. Their basic unit of animation is UIView
, which is a pretty heavyweight object, so you need to be careful about how many of them you use. UIView
also doesn’t support three-dimensional layout, except for basic z-ordering, so it can’t create anything like Cover Flow. To move your UI to the next level, you need to use Core Animation.
Core Animation provides a variety of tools, several of which are useful even if you don’t intend to animate anything. The most basic and important part of Core Animation is CALayer
. This section explains how to draw with CALayer
without animations. You explore animating later in the chapter.
In many ways, CALayer
is very much like UIView
. It has a location, size, transform, and content. You can override a draw method to draw custom content, usually with Core Graphics. There is a layer hierarchy exactly like the view hierarchy. You might ask, why even have separate objects?
The most important answer is that UIView
is a fairly heavyweight object that manages drawing and event handling, particularly touch events. CALayer
is all about drawing. In fact, UIView
relies on a CALayer
to manage its drawing, which allows the two to work very well together.
Every UIView
has a CALayer
to do its drawing. And every CALayer
can have sublayers, just like every UIView
can have subviews. Figure 9-2 shows the hierarchy.
A layer draws whatever is in its contents
property, which is a CGImage
(see the note at the end of this section). It’s your job to set this somehow, and there are various ways of doing so. The simplest approach is to assign it directly, as shown here (and discussed more fully in “Setting Contents Directly” later in this section).
UIImage *image = ...;
CALayer *layer = ...;
layer.contents = (__bridge id)[image CGImage];
View and layer hierarchies
If you do not set the contents
property directly, then Core Animation will go through the following CALayer
and delegate methods in the order presented in the following list to create it.
1. [CALayer
setNeedsDisplay]
—Your code needs to call this. It marks the layer as dirty, requesting that contents
be updated using the following steps in this list.
Unless setNeedsDisplay
is called, the contents
property is never updated, even if it’s nil
.
2. [CALayer displayIfNeeded]
—The drawing system automatically calls this as needed. If the layer has been marked dirty by a call to setNeedsDisplay
, then the drawing system will continue with the next steps.
3. [CALayer display]
—This is called by displayIfNeeded
when appropriate. You shouldn’t call it directly. The default implementation calls the delegate method displayLayer:
if the delegate implements it. If not, display
calls drawInContext:
. You can override display
in a subclass to set contents
directly.
4. [delegate displayLayer:]
—The default [CALayer display]
calls this if the delegate implements it. Its job is to set contents
. If this method is implemented, even if it does nothing, then no further custom drawing code will be run.
5. [CALayer drawInContext:]
—The default display
method creates a view graphics context and passes it to drawInContext:
. This is similar to [UIView drawRect:]
, but no UIKit context is set up for you automatically. To draw with UIKit, you need to call UIGraphicsPushContext()
to make the passed context the current context. Otherwise, just use the passed context to draw with Core Graphics. The default display
method takes the resulting context, creates a CGImage
(see note below) and assigns it to contents
. The default [CALayer drawInContext:]
calls [delegate drawLayer:inContext:]
if it’s implemented. Otherwise, it does nothing. Note that you may call this directly. See the section “Drawing in Your Own Context,” later in this section, for information on why you would call this directly.
6. [delegate drawLayer:inContext:]
—If implemented, the default drawInContext:
calls this to update the context so that display
can create a CGImage
.
As you can see, there are several ways to set the contents of a layer. You can set it directly with setContent:
, you can implement display
or displayLayer:
, or you can implement drawInContext:
or drawLayer:inContext:
. In the rest of this section, we discuss each approach.
The drawing system almost never automatically updates contents
in the way that UIView
is often automatically refreshed. For instance, UIView
draws itself the first time it’s put onscreen. CALayer
does not. Marking a UIView
as dirty with setNeedsDisplay
automatically redraws all the subviews as well. Marking a CALayer
as dirty with setNeedsDisplay
doesn’t impact sublayers. The thing to remember is that the default behavior of a UIView
is to draw when it thinks you need it. The default behavior of a CALayer
is to never draw unless you explicitly ask for it. CALayer
is a much lower-level object, and it’s optimized to not waste time doing anything that isn’t explicitly asked for.
The contents
property is usually a CGImage
, but this is not always the case. If you use custom drawing, Core Animation will use a private class, CABackingStorage
, for contents
. You can set contents
to either a CGImage
or the contents
of another layer.
Setting Contents Directly
Providing a content image (shown in the following code) is the easiest solution if you already have an image handy.
LayersViewController.m (Layers)
#import <QuartzCore/QuartzCore.h>
...
UIImage *image = [UIImage imageNamed:@”pushing.png”];
self.view.layer.contents = (__bridge id)[image CGImage];
You must always import QuartzCore.h
and link with QuartzCore.framework
to use Core Animation. This is an easy thing to forget.
The cast to __bridge id
is needed because contents
is defined as an id
, but actually expects a CGImageRef
. To make this work with ARC, a cast is required. A common error is to pass a UIImage
here instead of a CGImageRef
. You won’t get a compiler error or runtime warning. Your view will just be blank.
By default, the contents are scaled to fill the view, even if that distorts the image. As with contentMode
and contentStretch
in UIView
, you can configure CALayer
to scale its image in different ways using contentsCenter
and contentsGravity
.
Implementing Display
The job of display
or displayLayer:
is to set contents
to a correct CGImage
. You can do this any way you’d like. The default implementation creates a CGContext
, passes it to drawInContext:
, turns the result into a CGImage
, and assigns it to contents
. The most common reason to override this is if your layer has several states and you have an image for each. Buttons often work this way. You can create those images by loading them from your bundle, drawing them with Core Graphics, or any other way you’d like.
Whether to subclass CALayer
or use a delegate is really a matter of taste and convenience. UIView
has a layer, and it must be that layer’s delegate. In our experience, it’s dangerous to make a UIView
the delegate for any of the sublayers. Doing so can create infinite recursion when the UIView
tries to copy its sublayers in certain operations such as transitions. So you can implement displayLayer:
in UIView
to manage its layer, or you can have some other object be the delegate for sublayers.
Having UIView
implement displayLayer:
seldom makes sense in our opinion. If your view content is basically several images, it’s usually a better idea to use a UIImageView
or a UIButton
rather than a custom UIView
with hand-loaded layer content. UIImageView
is highly optimized for displaying images. UIButton
is very good at switching images based on state, and includes a lot of good user interface mechanics that are a pain to reproduce. Don’t try to reinvent UIKit in Core Animation. UIKit likely does it better than you will.
What can make more sense is to make your UIViewController
the delegate for the layers, particularly if you aren’t subclassing UIView
. This avoids extra objects and subclasses if your needs are pretty simple. Just don’t let your UIViewController
become overcomplicated.
Custom Drawing
As with UIView
, you can provide completely custom drawing with CALayer
. Typically, you’ll draw with Core Graphics, but using UIGraphicsPushContext
, you can also draw with UIKit.
See Chapter 8 for information on how to draw with Core Graphics and UIKit.
Using drawInContext:
is just another way of setting contents
. It’s called by display
, which is called only when the layer is explicitly marked dirty with setNeedsDisplay
. The advantage of this over setting contents
directly is that display
automatically creates a CGContext
appropriate for the layer. In particular, the coordinate system is flipped for you. (See Chapter 8 for a discussion of Core Graphics and flipped coordinate systems.) The following code shows how to implement the delegate method drawLayer:inContext:
to draw the string “Pushing The Limits” at the top of the layer using UIKit. Because Core Animation does not set a UIKit graphics context, you need to call UIGraphicsPushContext
before calling UIKit methods, and UIGraphicsPopContext
before returning.
DelegateView.m (Layers)
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self.layer setNeedsDisplay];
}
return self;
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
UIGraphicsPushContext(ctx);
[[UIColor whiteColor] set];
UIRectFill(layer.bounds);
[[UIColor blackColor] set];
UIFont *font = [UIFont systemFontOfSize:48.0];
[@”Pushing The Limits” drawInRect:[layer bounds]
withFont:font
lineBreakMode:NSLineBreakByWordWrapping
alignment:NSTextAlignmentCenter];
UIGraphicsPopContext();
}
Note the call to setNeedsDisplay
in initWithFrame:
. As discussed earlier, layers don’t automatically draw themselves when put onscreen. You need to mark them as dirty with setNeedsDisplay
.
You may also notice the hand-drawing of the background rather than using the backgroundColor
property. This is intentional. Once you engage in custom drawing with drawLayer:inContext:
, most automatic layer settings like backgroundColor
and cornerRadius
are ignored. Your job in drawLayer:inContext:
is to draw everything needed for the layer. There isn’t helpful compositing going on for you like in UIView
. If you want layer effects like rounded corners together with custom drawing, then put the custom drawing onto a sublayer and round the corners on the superlayer.
Drawing in Your Own Context
Unlike [UIView drawRect:]
, it’s completely legal to call [CALayer drawInContext:]
yourself. You just need to generate a context and pass it in, which is nice for capturing the contents of a layer onto a bitmap or PDF so you can save it or print it. Calling drawInContext:
this way is mostly useful if you want to composite this layer with something else, because, if all you want is a bitmap, you can just use contents
.
drawInContext:
only draws the current layer, not any of its sublayers. If you want to draw the layer and its sublayers, use renderInContext:
, which also captures the current state of the layer if it’s animating. renderInContext:
uses the current state of the render tree that Core Animation maintains internally, so it doesn’t call drawInContext:
.
Moving Things Around
Now that you can draw in a layer, take a look into how to use those layers to create powerful animations.
Layers naturally animate. In fact, you need to do a small amount of work to prevent them from animating. Consider the following example.
LayerAnimationViewController.m (LayerAnimation)
- (void)viewDidLoad {
[super viewDidLoad];
CALayer *squareLayer = [CALayer layer];
squareLayer.backgroundColor = [[UIColor redColor] CGColor];
squareLayer.frame = CGRectMake(100, 100, 20, 20);
[self.view.layer addSublayer:squareLayer];
UIView *squareView = [UIView new];
squareView.backgroundColor = [UIColor blueColor];
squareView.frame = CGRectMake(200, 100, 20, 20);
[self.view addSubview:squareView];
[self.view addGestureRecognizer:
[[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(drop:)]];
}
- (void)drop:(UIGestureRecognizer *)recognizer {
NSArray *layers = self.view.layer.sublayers;
CALayer *layer = [layers objectAtIndex:0];
[layer setPosition:CGPointMake(200, 250)];
NSArray *views = self.view.subviews;
UIView *view = [views objectAtIndex:0];
[view setCenter:CGPointMake(100, 250)];
}
This draws a small red sublayer and a small blue subview. When the view is tapped, both are moved. The view jumps immediately to the new location. The layer animates over a quarter-second. It’s fast, but it’s not instantaneous like the view.
CALayer
implicitly animates all properties that support animation. You can prevent this by disabling actions:
[CATransaction setDisableActions:YES];
We discuss actions further in the “Auto-Animate with Actions” section, later in this chapter.
disableActions
is a poorly named method. Because it begins with a verb, you expect it to have a side effect (disabling actions) rather than returning the current value of the property. It should be actionsDisabled
(or actionsEnabled
to be parallel with userInteractionEnabled
). Apple may remedy this eventually, as it has with other misnamed properties. In the meantime, make sure to call setDisableActions:
when you mean to change its value. You won’t get a warning or error if you call [CATransaction disableActions]
in a void context.
Implicit Animations
You now know all the basics of animation. Just set layer properties, and your layers animate in the default way. But what if you don’t like the defaults? For instance, you may want to change the duration of the animation. First, you need to understand transactions.
Most of the time when you change several layer properties, you want them all to animate together. You also don’t want the renderer to waste the renderer’s time calculating animations for one property change if the next property change affects it. For instance, opacity
and backgroundColor
are interrelated properties. Both affect the final displayed pixel color, so the renderer needs to know about both animations when working out the intermediate values.
Core Animation bundles property changes into atomic transactions (CATransaction
). An implicit CATransaction
is created for you the first time you modify a layer on a thread that includes a run loop. (If that last sentence piqued your interest, see the “Core Animation and Threads” section, later in this chapter.) During the run loop, all layer changes are collected, and when the run loop completes, all the changes are committed to the layer tree.
To modify the animation properties, you need to make changes to the current transaction. The following changes the duration of the current transaction to 2 seconds rather than the default quarter-second.
[CATransaction setAnimationDuration:2.0];
You can also set a completion block to run after the current transaction finishes animating using [CATransaction setCompletionBlock:]
. You can use this to chain animations together, among other things.
Although the run loop creates a transaction for you automatically, you can also create your own explicit transactions using [CATransaction begin]
and [CATransaction commit]
. These allow you to assign different durations to different parts of the animation or to disable animations for only a part of the event loop.
See the “Auto-Animate with Actions” section of this chapter for more information on how implicit animations are implemented and how you can extend them.
Explicit Animations
Implicit animations are powerful and convenient, but sometimes you want more control. That’s where CAAnimation
comes in. With CAAnimation
, you can manage repeating animations, precisely control timing and pacing, and employ layer transitions. Implicit animations are implemented using CAAnimation
, so everything you can do with an implicit animation can be done explicitly as well.
The most basic animation is a CABasicAnimation
. It interpolates a property over a range using a timing function, as shown in the following code:
CABasicAnimation *anim = [CABasicAnimation
animationWithKeyPath:@”opacity”];
anim.fromValue = [NSNumber numberWithDouble:1.0];
anim.toValue = [NSNumber numberWithDouble:0.0];
anim.autoreverses = YES;
anim.repeatCount = INFINITY;
anim.duration = 2.0;
[layer addAnimation:anim forKey:@”anim”];
This pulses the layer forever, animating the opacity from one to zero and back over 2 seconds. When you want to stop the animation, remove it:
[layer removeAnimationForKey:@”anim”];
An animation has a key
, fromValue
, toValue
, timingFunction
, duration
, and some other configuration options. The way it works is to make several copies of the layer, send setValue:forKey:
messages to the copies and then display
. It captures the generated contents
and displays them.
If you have custom properties in your layer, you may notice that they’re not set correctly during animation. This is because the layer is copied. You must override initWithLayer:
to copy your custom properties if you want them to be available during an animation. We discuss this later in the “Animating Custom Properties” section of this chapter.
CABasicAnimations
are basic, as the name implies. They’re easy to set up and use, but they’re not very flexible. If you want more control over the animation, you can move to CAKeyframeAnimation
. The major difference is that instead of giving a fromValue
and toValue
, you now can give a path or a sequence of points to animate through, along with individual timing for each segment. The Animation Types and Timing Programming Guide at developer.apple.com
provides excellent examples. They’re not technically difficult to set up. Most of the work is on the creative side to find just the right path and timing.
Model and Presentation
A common problem in animations is the dreaded “jump back.” The mistake looks like this:
CABasicAnimation *fade;
fade = [CABasicAnimation animationWithKeyPath:@”opacity”];
fade.duration = 1;
fade.fromValue = [NSNumber numberWithDouble:1.0];
fade.toValue = [NSNumber numberWithDouble:0.0];
[circleLayer addAnimation:fade forKey:@”fade”];
This fades the circle out over 1 second, just as expected, and then suddenly the circle reappears. To understand why this happens, you need to be aware of the difference between the model layer and the presentation layer.
The model layer is defined by the properties of the “real” CALayer
object. Nothing in the preceding code modifies any property of circleLayer
itself. Instead, CAAnimation
makes copies of circleLayer
and modifies those. These become the presentation layer. They represent roughly what is shown on the screen. There is technically another layer called the render layer that really represents what’s on the screen, but it’s internal to Core Animation, and you very seldom encounter it.
So what happens in the preceding code? CAAnimation
modifies the presentation layer, which is drawn to the screen, and when it completes, all its changes are thrown away and the model layer is used to determine the new state. The model layer hasn’t changed, so you snap back to where you started. The solution to this is to set the model layer, as shown here:
circleLayer.opacity = 0;
CABasicAnimation *fade;
fade = [CABasicAnimation animationWithKeyPath:@”opacity”];
...
[circleLayer addAnimation:fade forKey:@”fade”];
Sometimes this works fine, but sometimes the implicit animation in setOpacity:
fights with the explicit animation from animationWithKeyPath:
. The best solution to that is to turn off implicit animations if you’re doing explicit animations:
[CATransaction begin];
[CATransaction setDisableActions:YES];
circleLayer.opacity = 0;
CABasicAnimation *fade;
fade = [CABasicAnimation animationWithKeyPath:@”opacity”];
...
[circleLayer addAnimation:fade forKey:@”fade”];
[CATransaction commit];
Sometimes you see people recommend setting removedOnCompletion
to NO
and fillMode
to kCAFillModeBoth
. This is not a good solution. It essentially makes the animation go on forever, which means the model layer is never updated. If you ask for the property’s value, you continue to see the model value, not what you see on the screen. If you try to implicitly animate the property afterward, it won’t work correctly because the CAAnimation
is still running. If you ever remove the animation by replacing it with another with the same name, calling removeAnimationForKey:
or removeAllAnimations
, the old value snaps back. On top of all of that, it wastes memory.
All of this becomes a bit of a pain, so you may like the following category on CALayer
that wraps it all together and lets you set the duration and delay. Most of the time, I still prefer implicit animation, but this can make explicit animation a bit simpler.
CALayer+RNAnimation.m (LayerAnimation)
@implementation CALayer (RNAnimations)
- (void)setValue:(id)value
forKeyPath:(NSString *)keyPath
duration:(CFTimeInterval)duration
delay:(CFTimeInterval)delay
{
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self setValue:value forKeyPath:keyPath];
CABasicAnimation *anim;
anim = [CABasicAnimation animationWithKeyPath:keyPath];
anim.duration = duration;
anim.beginTime = CACurrentMediaTime() + delay;
anim.fillMode = kCAFillModeBoth;
anim.fromValue = [[self presentationLayer] valueForKey:keyPath];
anim.toValue = value;
[self addAnimation:anim forKey:keyPath];
[CATransaction commit];
}
@end
A Few Words on Timings
As in the universe at large, in Core Animation, time is relative. A second does not always have to be a second. Just like coordinates, time can be scaled.
CAAnimation
conforms to the CAMediaTiming
protocol, and you can set the speed
property to scale its timing. Because of this, when considering timings between layers, you need to convert them just like you need to convert points that occur in different views or layers.
localPoint = [self convertPoint:remotePoint fromLayer:otherLayer];
localTime = [self convertTime:remotetime fromLayer:otherLayer];
This isn’t very common, but it comes up when you’re trying to coordinate animations. You might ask another layer for a particular animation and when that animation will end so that you can start your animation.
CAAnimation *otherAnim = [layer animationForKey:@”anim”];
CFTimeInterval finish = otherAnim.beginTime + otherAnim.duration;
myAnim.beginTime = [self convertTime:finish fromLayer:layer];
Setting beginTime
like this is a nice way to chain animations, even if you hard-code the time rather than ask the other layer. To reference “now,” just use CACurrentMediaTime()
.
This raises another issue, however. What value should your property have between now and when the animation begins? You would assume that it would be the fromValue
, but that isn’t how it works. It’s the current model value because the animation hasn’t begun. Typically, this is the toValue
. Consider the following animation:
[CATransaction begin];
anim = [CABasicAnimation animationWithKeyPath:@”opacity”];
anim.fromValue = [NSNumber numberWithDouble:1.0];
anim.toValue = [NSNumber numberWithDouble:0.5];
anim.duration = 5.0;
anim.beginTime = CACurrentMediaTime() + 3.0;
[layer addAnimation:anim forKey:@”fade”];
layer.opacity = 0.5;
[CATransaction commit];
This animation does nothing for 3 seconds. During that time, the default property animation is used to fade opacity
from 1.0 to 0.5. Then the animation begins, setting the opacity to its fromValue
and interpolating to its toValue
. So the layer begins with opacity
of 1.0, fades to 0.5 over a quarter-second, then 3 seconds later, and jumps back to 1.0 and fades again to 0.5 over 5 seconds. This almost certainly isn’t what you want.
You can resolve this problem using fillMode
. The default is kCAFillModeRemoved
, which means that the animation has no influence on the values before or after its execution. This can be changed to “clamp” values before or after the animation by setting the fill mode to kCAFillModeBackwards
, kCAFillModeForwards
, or kCAFillModeBoth
. Figure 9-3 illustrates this.
In most cases, you want to set fillMode
to kCAFillModeBackwards
or kCAFillModeBoth
.
Into the Third Dimension
Chapter 8 discussed how to use CAAffineTransform
to make UIView
drawing much more efficient. This technique limits you to two-dimensional transformations: translate, rotate, scale, and skew. With layers, however, you can apply three-dimensional transformations by adding perspective. This is often called 2.5D rather than 3D because it doesn’t make layers into truly three-dimensional objects in the way that OpenGL ES does. But it does allow you to give the illusion of three-dimensional movement.
You rotate layers around an anchor point. By default, the anchor point is in the center of the layer, designated {0.5, 0.5}. It can be moved anywhere within the layer, making it convenient to rotate around an edge or corner. The anchor point is described in terms of a unit square rather than in points. So the lower-right corner is {1.0, 1.0}, no matter how large or small the layer is.
Here’s a simple example of a three-dimensional box.
BoxViewController.h (Box)
@interface BoxViewController : UIViewController
@property (nonatomic, readwrite, strong) CALayer *topLayer;
@property (nonatomic, readwrite, strong) CALayer *bottomLayer;
@property (nonatomic, readwrite, strong) CALayer *leftLayer;
@property (nonatomic, readwrite, strong) CALayer *rightLayer;
@property (nonatomic, readwrite, strong) CALayer *frontLayer;
@property (nonatomic, readwrite, strong) CALayer *backLayer;
@end
Effect of fill modes on media timing functions
BoxViewController.m (Box)
@implementation BoxViewController
const CGFloat kSize = 100.;
const CGFloat kPanScale = 1./100.;
- (CALayer *)layerWithColor:(UIColor *)color
transform:(CATransform3D)transform {
CALayer *layer = [CALayer layer];
layer.backgroundColor = [color CGColor];
layer.bounds = CGRectMake(0, 0, kSize, kSize);
layer.position = self.view.center;
layer.transform = transform;
[self.view.layer addSublayer:layer];
return layer;
}
static CATransform3D MakePerspetiveTransform() {
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1./2000.;
return perspective;
}
- (void)viewDidLoad {
[super viewDidLoad];
CATransform3D transform;
transform = CATransform3DMakeTranslation(0, -kSize/2, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1.0, 0, 0);
self.topLayer = [self layerWithColor:[UIColor redColor]
transform:transform];
transform = CATransform3DMakeTranslation(0, kSize/2, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1.0, 0, 0);
self.bottomLayer = [self layerWithColor:[UIColor greenColor]
transform:transform];
transform = CATransform3DMakeTranslation(kSize/2, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
self.rightLayer = [self layerWithColor:[UIColor blueColor]
transform:transform];
transform = CATransform3DMakeTranslation(-kSize/2, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
self.leftLayer = [self layerWithColor:[UIColor cyanColor]
transform:transform];
transform = CATransform3DMakeTranslation(0, 0, -kSize/2);
transform = CATransform3DRotate(transform, M_PI_2, 0, 0, 0);
self.backLayer = [self layerWithColor:[UIColor yellowColor]
transform:transform];
transform = CATransform3DMakeTranslation(0, 0, kSize/2);
transform = CATransform3DRotate(transform, M_PI_2, 0, 0, 0);
self.frontLayer = [self layerWithColor:[UIColor magentaColor]
transform:transform];
self.view.layer.sublayerTransform = MakePerspetiveTransform();
UIGestureRecognizer *g = [[UIPanGestureRecognizer alloc]
initWithTarget:self
action:@selector(pan:)];
[self.view addGestureRecognizer:g];
}
- (void)pan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view];
CATransform3D transform = MakePerspetiveTransform();
transform = CATransform3DRotate(transform,
kPanScale * translation.x,
0, 1, 0);
transform = CATransform3DRotate(transform,
-kPanScale * translation.y,
1, 0, 0);
self.view.layer.sublayerTransform = transform;
}
@end
BoxViewController
shows how to build a simple box and rotate it based on panning. All the layers are created with layerWithColor:transform:
. Notice that all the layers have the same position
. They only appear to be in the shape of a box through transforms that translate and rotate them.
You apply a perspective sublayerTransform
(a transform applied to all sublayers, but not the layer itself). We won’t go into the math here, but the m34
position of the 3D transform matrix should be set to -1/EYE_DISTANCE
. For most cases, 2000 units works well, but you can adjust this to “zoom the camera.”
You could also build this box by setting position
and zPosition
rather than translating, as shown in the following code. This may be more intuitive for some developers.
BoxTransformViewController.m (BoxTransform)
- (CALayer *)layerAtX:(CGFloat)x y:(CGFloat)y z:(CGFloat)z
color:(UIColor *)color
transform:(CATransform3D)transform {
CALayer *layer = [CALayer layer];
layer.backgroundColor = [color CGColor];
layer.bounds = CGRectMake(0, 0, kSize, kSize);
layer.position = CGPointMake(x, y);
layer.zPosition = z;
layer.transform = transform;
[self.contentLayer addSublayer:layer];
return layer;
}
- (void)viewDidLoad {
[super viewDidLoad];
CATransformLayer *contentLayer = [CATransformLayer layer];
contentLayer.frame = self.view.layer.bounds;
CGSize size = contentLayer.bounds.size;
contentLayer.transform =
CATransform3DMakeTranslation(size.width/2, size.height/2, 0);
[self.view.layer addSublayer:contentLayer];
self.contentLayer = contentLayer;
self.topLayer = [self layerAtX:0 y:-kSize/2 z:0
color:[UIColor redColor]
transform:MakeSideRotation(1, 0, 0)];
...
}
- (void)pan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view];
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform,
kPanScale * translation.x,
0, 1, 0);
transform = CATransform3DRotate(transform,
-kPanScale * translation.y,
1, 0, 0);
self.view.layer.sublayerTransform = transform;
}
You now need to insert a CATransformLayer
to work with. If you just use a CALayer
, then zPosition
is used only for calculating layer order. It’s not used to determine location in space. This makes the box look completely flat. CATransformLayer
supports zPosition
without requiring you to apply a perspective transform.
Decorating Your Layers
A major advantage of CALayer
over UIView
, even if you’re working only in 2D, is the automatic border effects that CALayer
provides. For instance, CALayer
can automatically give you rounded corners, a colored border, and a drop shadow. All of these can be animated, which can provide some nice visual effects. For instance, you can adjust the position and shadow to give the illusion of clicking as the user presses and releases a layer. The following code will create the layer shown in Figure 9-4.
DecorationViewController.m (Decoration)
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
layer.cornerRadius = 10;
layer.backgroundColor = [[UIColor redColor] CGColor];
layer.borderColor = [[UIColor blueColor] CGColor];
layer.borderWidth = 5;
layer.shadowOpacity = 0.5;
layer.shadowOffset = CGSizeMake(3.0, 3.0);
[self.view.layer addSublayer:layer];
Layer with colored, rounded border and shadow
Auto-Animate with Actions
Most of the time, implicit animations do what you want, but there are times you’d like to configure them. You can turn off all implicit animations using CATransaction
, but that applies only to the current transaction (generally the current run loop). To modify how an implicit animation behaves, and especially if you want it to always behave that way for this layer, you need to configure the layer’s actions. This allows you to configure your animations when you create the layer rather than apply an explicit animation every time you change a property.
Layer actions are fired in response to various changes on the layer, such as adding or removing the layer from the hierarchy or modifying a property. When you modify the position
property, for instance, the default action is to animate it over a quarter-second. In the following examples, CircleLayer
is a layer that draws a red circle in its center with the given radius
.
ActionsViewController.m (Actions)
CircleLayer *circleLayer = [CircleLayer new];
circleLayer.radius = 20;
circleLayer.frame = self.view.bounds;
[self.view.layer addSublayer:circleLayer];
...
[circleLayer setPosition:CGPointMake(100, 100)];
Let’s modify this so that changes in position always animate over 2 seconds:
CircleLayer *circleLayer = [CircleLayer new];
circleLayer.radius = 20;
circleLayer.frame = self.view.bounds;
[self.view.layer addSublayer:circleLayer];
CABasicAnimation *anim =
[CABasicAnimation animationWithKeyPath:@”position”];
anim.duration = 2;
NSMutableDictionary *actions =
[NSMutableDictionary dictionaryWithDictionary:
[circleLayer actions]];
[actions setObject:anim forKey:@”position”];
circleLayer.actions = actions;
...
[circleLayer setPosition:CGPointMake(100, 100)];
Setting the action to [NSNull null]
disables implicit animations for that property. A dictionary cannot hold nil
, so you need to use the NSNull
class.
There are some special actions for when the layer is added to the layer tree (kCAOnOrderIn
) and when it’s removed (kCAOnOrderOut
). For example, you can make a group animation of growing and fade-in like this:
CABasicAnimation *fadeAnim = [CABasicAnimation
animationWithKeyPath:@”opacity”];
fadeAnim.fromValue = [NSNumber numberWithDouble:0.4];
fadeAnim.toValue = [NSNumber numberWithDouble:1.0];
CABasicAnimation *growAnim = [CABasicAnimation
animationWithKeyPath:
@”transform.scale”];
growAnim.fromValue = [NSNumber numberWithDouble:0.8];
growAnim.toValue = [NSNumber numberWithDouble:1.0];
CAAnimationGroup *groupAnim = [CAAnimationGroup animation];
groupAnim.animations = [NSArray arrayWithObjects:fadeAnim,
growAnim, nil];
[actions setObject:groupAnim forKey:kCAOnOrderIn];
Actions are also important when dealing with transitions (kCATransition
) when one layer is replaced with another. This is commonly used with a CATransition
(a special type of CAAnimation
). You can apply a CATransition
as the action for the contents
property to create special effects like slide show whenever the contents change. By default, the fade transition is used.
Animating Custom Properties
Core Animation implicitly animates several layer properties, but what about custom properties on CALayer
subclasses? For instance, in the CircleLayer
, you have a radius
property. By default, radius
is not animated, but contents
is (using a fade CATransition
). So changing the radius causes your current circle to cross-fade with your new circle. This probably isn’t what you want. You want radius
to animate just like position
. There are a few steps to make this work correctly, as shown in the following example.
CircleLayer.m (Actions)
@implementation CircleLayer
@dynamic radius;
- (id)init {
self = [super init];
if (self) {
[self setNeedsDisplay];
}
return self;
}
- (id)initWithLayer:(id)layer {
self = [super initWithLayer:layer];
[self setRadius:[layer radius]];
return self;
}
- (void)drawInContext:(CGContextRef)ctx {
CGContextSetFillColorWithColor(ctx,
[[UIColor redColor] CGColor]);
CGFloat radius = self.radius;
CGRect rect;
rect.size = CGSizeMake(radius, radius);
rect.origin.x = (self.bounds.size.width - radius) / 2;
rect.origin.y = (self.bounds.size.height - radius) / 2;
CGContextAddEllipseInRect(ctx, rect);
CGContextFillPath(ctx);
}
+ (BOOL)needsDisplayForKey:(NSString *)key {
if ([key isEqualToString:@”radius”]) {
return YES;
}
return [super needsDisplayForKey:key];
}
- (id < CAAction >)actionForKey:(NSString *)key {
if ([self presentationLayer] != nil) {
if ([key isEqualToString:@”radius”]) {
CABasicAnimation *anim = [CABasicAnimation
animationWithKeyPath:@”radius”];
anim.fromValue = [[self presentationLayer]
valueForKey:@”radius”];
return anim;
}
}
return [super actionForKey:key];
}
@end
We’ll start with a reminder of the basics. You call setNeedsDisplay
in init
so that the layer’s drawInContext:
is called the first time it’s added to the layer tree. You override needsDisplayForKey:
so that whenever radius
is modified, you automatically redraw.
Core Animation makes several copies of the layer in order to animate. It uses initWithLayer:
to perform the copy, so you need to implement that method to copy your custom properties.
Now you come to your actions. You implement actionForKey:
to return an animation with a fromValue
of the currently displayed (presentationLayer
) radius. This means that you’ll animate smoothly if the animation is changed midflight.
It’s critical to note that you implemented the radius
property using @dynamic
here, not @synthesize
. CALayer
automatically generates accessors for its properties at runtime, and those accessors have important logic. It’s vital that you not override these CALayer
accessors by either implementing your own accessors or using @synthesize
to do so.
Core Animation and Threads
It’s worth noting that Core Animation is very tolerant of threading. You can generally modify CALayer
properties on any thread, unlike UIView
properties. drawInContext:
may be called from any thread (although a given CGContext
should be modified on only one thread at a time). Changes to CALayer
properties are batched into transactions using CATransaction
. This happens automatically if you have a run loop. If you don’t have a run loop, you need to call [CATransaction flush]
periodically. If at all possible, though, you should perform Core Animation actions on a thread with a run loop to improve performance.
Summary
Core Animation is one of the most important frameworks in iOS. It puts a fairly easy-to-use API in front of an incredibly powerful engine. There are still a few rough edges to it, however, and sometimes things need to be “just so” to make it work correctly (for example, implementing your properties with @dynamic
rather than @synthesize
). When it doesn’t work correctly, it can be challenging to debug, so having a good understanding of how it works is crucial. Hopefully, this chapter has made you confident enough with the architecture and the documentation to dive in and make some really beautiful apps.
Further Reading
Apple Documentation
The following documents are available in the iOS Developer Library at developer.apple.com
or through the Xcode Documentation and API Reference.
Animation Types and Timing Programming Guide
Core Animation Programming Guide
Other Resources
millen.me. (Milen Dzhumerov), “CA’s 3D Model” An excellent overview of the math behind the perspective transform, including the magic -1/2000.http://milen.me/technical/core-animation-3d-model/
Cocoa with Love, (Matt Gallagher), “Parametric acceleration curves in Core Animation,” Explains how to implement timing curves that cannot be implemented with CAMediaTimingFunction
, such as damped ringing and exponential decay.cocoawithlove.com/2008/09/parametric-acceleration-curves-in-core.html