iOS Animation With Layers

This article describes advanced techniques for native animations on iOS. Specifically, it shows how to animate bezier curves, labels and their content. These animations are more advanced than the standard UIView animation blocks introduced in iOS 4.0, because the latter is limited to merely a subset of animatable properties that iOS offers. As the underlying techniques are based on Quartz 2D the following will also work on macOS, although this requires changing the UIKit components to their Cocoa equivalents.

Introduction

The fourth generation of the iPhone, the iPod Touch as well as the third generation of the iPad offer increasingly powerful graphics hardware. This is useful not only for games, but also for improved user interfaces, and a better user experience.

This articles covers an important aspect of the fundamental graphics API of iOS – native support for animations. For an introduction to the basic Quartz API please consult the online documentation. It is useful to be familiar with the basic concepts of Quartz before proceeding, but anybody with thorough knowledge of that manual will most likely not need to read this article.

Basic Animations

The easiest way to add animations to iOS apps is to use the block-based animations that all instances of UIView and their sub-classes support:

1
2
3
4
5
6
7
[UIView animateWithDuration:10
                 animations:^{
                     myLabel.frame = CGRectMake([self.view frame].size.width - 100,
                                                [self.view frame].size.height - 50,
                                                self.myLabel.frame.size.width,
                                                self.myLabel.frame.size.height);
                 }];

This command triggers an animation with a duration of ten seconds and smoothly varies the location and the size of a view myLabel. For the starting and ending phase of the animation it uses the Smoothstep function by default.

Note that this animation uses a class method of UIView, even though the animation actually modifies properties in specific instances of subclasses of UIView.

A drawback of this approach is that there is no callback (delegate) feature that could get called and refer to the current state of the animation. Furthermore, the animateWithDuration:… methods all return immediately, and the view’s properties adopt their final destination values immediately – even while the animation is still in progress. This results in a mismatch of the view’s current properties on the one hand and the actual state on screen. We will see below how to deal with this situation and resolve this discrepancy below.

Animating Curves and Paths

Basic Animation Revisited

For the animations discussed below we need to rewrite the above call as follows:

1
2
3
4
5
6
7
8
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.duration = 10.f;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake([self.view frame].size.width - 100,
                                                          [self.view frame].size.height - 50)];
[myLabel.layer addAnimation:animation
                     forKey:@"position"];

This approach looks a little more “old-school”, but it is necessary for animating other things than the elementary view properties discussed above. It is, in fact, the fundamental native interface to layer animation.

Drawing Paths

A path is another fundamental concept of Quartz 2D. It is a collection of individual components like lines and arcs that can be drawn and animated. The following code constructs an elementary path with three lines that make up a triangle:

1
2
3
4
5
CGMutablePathRef aPath = CGPathCreateMutable();
CGPathMoveToPoint(aPath, NULL, 50, 150);
CGPathAddLineToPoint(aPath, NULL, 150, 200);
CGPathAddLineToPoint(aPath, NULL, 50, 250);
CGPathCloseSubpath(aPath);

One way to use a path is by setting the path property of a CAShapeLayer. All the drawing-related information likes shadows, fills and colors are configured on the CAShapeLayer. Thus, the path could be drawn in a view as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
CALayer *layer = [self layer];
self.backgroundColor = [UIColor clearColor];
self.shapeLayer = [CAShapeLayer layer];
self.shapeLayer.backgroundColor = [[UIColor brownColor] CGColor];
self.shapeLayer.lineWidth = 2;
self.shapeLayer.strokeColor = [[UIColor redColor] CGColor];
self.shapeLayer.fillColor = [[UIColor greenColor] CGColor];
self.shapeLayer.shadowRadius = 10;
self.shapeLayer.shadowColor = [[UIColor blackColor] CGColor];
self.shapeLayer.shadowOffset = CGSizeMake(3, 7);
self.shapeLayer.shadowOpacity = 0.5;
[layer addSublayer:self.shapeLayer];

// Insert the path code from above to create the aPath object.
self.shapeLayer.path = aPath;
CFRelease(aPath);

It is important to manually release any path object that has been created with CGPathCreateMutable() as this function is a C-style interface and thus not handled by automatic reference counting (ARC). Outside a subclass of UIView the reference to self must be replaced by an appropriate different instance.

Animating Paths

The basic animations of UIView cannot be used to animate path objects introduced above. The more fundamental animations of class CABasicAnimation, however, can. The following code animates the path property of a CAShapeLayer object:

1
2
3
4
5
6
7
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
animation.duration = 10.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (__bridge_transfer id)aPath;
animation.toValue = (__bridge_transfer id)anotherPath;
[myTestView.shapeLayer addAnimation:animation
                             forKey:@"animatePath"];

This code fragment needs two objects of class CGMutablePathRef. One is the starting path, in this case aPath shown above. The other one is the shape that is supposed to be shown at the end, denoted by anotherPath. With this approach it is possible to animate arbitrary shapes and objects on screen without needing to compute intermediate points and locations. It is entirely sufficient to only provide the starting and ending shapes and Quartz figures out how to do the rest!

It is also possible to configure animations to repeat and/or reverse by setting appropriate options on the instance of CABasicAnimation:

1
2
animation.repeatCount = 1.e10f;
animation.autoreverses = YES;

Fills, Shadows, 3D — Oh My!

The above code demonstrates how not only elementary shapes can be drawn and animated, but all relevant properties set in the embedding layer will be used in the transitions. The CAShapeLayer object introduced above had a custom line width, line stroke color, filling color and shadow. Setting these properties automatically drew the shadow and filling on the path during the entire animation!

Quartz has several more subclasses of CALayer, most of which are fully animatable and thus can be used for stunning visual and animation effects:

CAGradientLayer
Draws colored gradient fills in the shape of its layer. Note that the colors themselves are animatable properties. See the developer documentation and note that this class makes use of the GPUs on modern iOS devices!
CATransformLayer
The CALayer object has a transform property which not only accepts a 2-dimensional transformation matrix, but also accepts a 3-dimensional CATransform3D struct. Needless to say that either one is animatable! This approach is kind of inconvenient, because it will flatten the sub-layers. One way to avoid this is to use a CATransformLayer that does not flatten its sub-layers and allows even other three-dimensional layers as sub-layers, see the developer documentation.
CAReplicatorLayer
The CAReplicatorLayer covers a surface with copies of one original layer. The really powerful feature of this approach is that it is possible to also let animations applied to the original layer be automagically deployed to the copies, resulting in extremely complex animation patterns with just a few lines of code. For macOS Apple has a demo, for iOS there is only the developer documentation.
The presentationLayer object instance
To access the current state of an ongoing animation use the presentationLayer property to investigate the current state of an animation. Unlike the animatable properties – which are set to their final destination values as soon as the animation call returns – the presentationLayer grants access to the current state of the animation and thus the animatable properties. Note that this may be a little more complicated, though, because the presentationLayer is an instance of CALayer, not of the original UIView whose properties are animated.

Basic and Advanced Animation of Labels

As UILabel is a sub-classes of UIView all the animatable properties discussed above are applicable. The code fragment shown above in the basic animations section is fully applicable here. However, there are a couple of additional things we can configure on labels while an animation is in progress. Even things that are not related to animatable properties! In the following I demonstrate a few of those.

Modifying the View Hierarchy and Content

A particularly powerful method to influence the presentation of labels is to use NSTimers to control the content of the label. This even works for the entire view hierarchy, as we can remove the label and reinsert it even as the animation progresses!

First, we set up a timer and demonstrate removing and re-adding the label:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// In the initialization part, set up the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1
                                 target:self
                               selector:@selector(callMe:)
                               userInfo:nil
                                repeats:YES];

// [...]
// Later on, implement the callMe: method.

- (void)callMe:(NSTimer *)sender
{
    self.myLabel.text = [NSString stringWithFormat:@"%d",
                         self.myCounter];

    // At t=3.0s remove the label.
    if (self.myCounter++ == 30) {
        [self.myLabel removeFromSuperview];
    }

    // At t=5.0s add the label again.
    if (self.myCounter == 50) {
        [self.view addSubview:self.myLabel];
    }
}

First, a timer is set up that calls the method callMe: every 0.1 seconds. Three seconds into the animation of the label, it is being removed from the view hierarchy and five seconds after the animation began it is being added again. Thus, it is possible to manipulate the view hierarchy during an ongoing animation.

Next, we go a step further and modify the content of the label text while it is being moved around:

1
2
3
// Add the following code to callMe:
self.myLabel.text = [NSString stringWithFormat:@"%d",
                     self.myCounter];

Now the label will contain a counter which updates every 0.1 seconds while it is being moved around.

Full Redraw of a Scene

Similar to the OpenGL callbacks it is possible to manually perform full screen redraws in case the animation procedures listed above are still insufficient. The basic mechanism is to use the class CADisplayLink, see the developer documentation. CADisplayLink is tied directly to the refresh rate of the display and can thus perform a full screen redraw while having access to the actual animation time. In this way it is the most powerful and generic way to circumvent the limitations of UIView animations discussed above.

A specific example of how to use CADisplayLink is given in this stackoverflow.com question, see the accepted answer.

Example Project

The techniques shown in this post can be combined and run asynchronously. In this way it is possible to build quite complex visualizations from basic building blocks.

A sample project with several of the techniques discussed above is available on github.com.