field-theory.org
the blog of wolfram schroers

Animations with layers on the iPhone

This article covers advanced techniques for implementing native animations on iOS. Specifically, it shows how to animate bezier curves, labels and their content on iOS. 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 animateable properties that iOS offers. The code presented in the following works on the iPhone, the iPod Touch and the iPad. As the underlying techniques are based on Quartz 2D it will also work MacOS X, although this requires changing the UIKit components to regular Cocoa classes.

Table of contents:

Introduction
Basic animations
Animating curves and paths
Basic and advanced animation of labels
Full redrawing of a scene
Download and conclusion

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:

    [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 one second and smoothly varies the location and the size of a view myLabel. For the starting and ending phase of the animation the Smoothstep function is used by default.

Note that this animation is set up using 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. Futhermore, the animateWithDuration:… methods all return immediately and the view's properties are set to 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

The basic animation revisited

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

    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. A path is a collection of individual components like lines and arcs that can be drawn and also animated. The following code constructs an elementary path with three lines that make up a triangle:

    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 object. Thus, the path could be drawn in a view as follows:

    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). If this code is used not in a subclass of UIView, then self must be replaced by an appropriate 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:

    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
    animation.duration = 20.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:

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

Fills, shadows, 3D — Oh my!

The above code has demonstrated that 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 animateable and thus can be used for stunning visual and animation effects:

CAGradientLayer
CAGradientLayer draws colored gradients filling the shape of its layer. Note that the colors themselves are animateable 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-dimentional CATransform3D struct. Needless to say that either one is animateable! This approach is kind of inconvenient, because it will flatten the sublayers, see this post for a discussion. The problem is resolved by using a CATransformLayer that does not flatten its sublayers and allows even other three-dimensional layers as sublayers, see the developer documentation.
CAReplicatorLayer
The CAReplicatorLayer helps covering 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 X there is a demo available, for iOS there is only the developer documentation.
The presentationLayer object instance
If access is needed to the current state of an ongoing animation, accessing the presentationLayer property can be used to investigate the current state of an animation. Unlike the animateable properties – which are set to their final destination values as soon as the animation call returns – the presentationLayer allows access to the current state of the animation and thus the animateable 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 animateable 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:

    // 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:

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

Now the label will contain a counter that 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 post on stackoverflow.com, see the accepted answer.

Download and conclusion

The techniques shown in this article can be combined and run asynchronously. In this way it is possible to build quite complex visualizations from basic building blocks. I have combined some of the animation techniques discussed above in a sample project. It is licensed under the GPLv3 and can be downloaded as a gzipped tar-ball.