5.1. Understanding Layers

A layer is a low-level component found in displayable classes. Layers act like a sheet of poster board to which an object's contents are affixed. It acts as a flexible backing for the object's display contents and can bend or contort the content in many ways on the screen. Every object that is capable of rendering itself—namely, those derived from the UIView class—has a layer to which its contents are glued.

For example, the UIImageView class contains all the basic information about a two-dimensional image—its display region, resolution, and various methods for working with and rendering the image. The image itself is glued to the parent UIView class's layer, which is like the backing in a picture frame. The most basic layer behaves like a single piece of cardboard, and merely presents the image as-is. Layers can also contain multiple sublayers, which can be treated like transparencies; each with different content, stacked on top of each other to produce one composite image.

Since they are flexible, you can use layers to manipulate the contents affixed to them. The layer's class, CALayer, controls how this flexible backing is manipulated as it's displayed. If you bend the layer, the image will bend with it. If you rotate the layer, the image will come along for the ride. You can transform the layer, add an animation, or rotate, skew, or scale the image as if it were sitting on top of silly putty. The object sitting on top of the layer is completely oblivious to how it is being manipulated, allowing your application to continue working with the displayed object as if it were still a two-dimensional object. When the user sees the image, however, it's been transformed to whatever orientation the layer has been twisted to. Layers can transform more than just images. The display output for all of the other view classes covered in Chapters Chapter 3 and Chapter 10 also rest on top of a layer.

To access the layer, read the base UIView class's layer property:

CALayer *layer = myView.layer;

All objects derived from UIView will inherit this property, meaning you can transform, scale, rotate, and even animate navigation bars, tables, text boxes, and many other types of view classes.

The important thing to remember about layers is that all UIView objects have one and they control the way in which its contents are ultimately rendered on the screen.

5.1.1. Layer Hierarchies

Layers have a number of general-purpose methods and properties to work with sublayers and perform rendering. These methods allow you to "stack" a number of individual screen layers together to render a single composite screen image.

A single layer can have many sublayers. Sublayers can be arranged and are stitched together when the final screen is rendered. Consider a first-person shooter. Your game might consist of one layer to render the game characters, one to render the background, and one to render a head-up display (HUD). You might have a dedicated UIView class for each, and a fourth class representing the game screen:

UIView *gameView = [ [ UIView alloc ] initWithFrame:
    [ [ UIScreen mainScreen ] applicationFrame ]
];

UIView *backgroundView = [ [ UIView alloc ] initWithFrame...
UIView *characterView = [ [ UIView alloc ] initWithFrame...
UIView *HUDView = [ [ UIView alloc ] initWithFrame...

You can anchor each of the three UIView class's layers to the gameView object using the CALayer class's addSublayer method:

#import <QuartzCore/QuartzCore.h>

CALayer *gameLayer = gameView.layer;
[ gameLayer addSublayer: backgroundView.layer ];
[ gameLayer addSublayer: characterView.layer ];
[ gameLayer addSublayer: HUDView.layer ];

When the gameView object is displayed on the screen, all three sublayers will be merged and rendered. Each class renders its own layer individually, but when the game layer is drawn, all three will be merged together.

The gameView layer isn't the only layer that you can add to. Sublayers can have sublayers of their own, and an entire hierarchy of layers can be built. For example, your game might add a layer to the HUDView layer to display one component of the HUD, such as a team logo:

UIImageView *teamLogoView = [ [ UIImageView alloc ] initWithImage: myTeamLogo ];
[ HUDView.layer addSublayer: teamLogoView.layer ];

                                          

Whenever the HUDView layer is rendered, all of its sublayers will also be rendered.

5.1.2. Size and Offset

To change the size or offset at which a layer is rendered, set the layer's frame property. The layer's frame functions in the same way as a window or view class's frame does: by accepting an x/y origin as an offset and region size. The example below sets the teamLogoView layers offset to 150x100, and its size to 50x75:

CGRect teamLogoFrame = CGRectMake(150.0, 100.0, 50.0, 75.0);
teamLogoView.layer.frame = teamLogoFrame;

Alternatively, you can adjust the position of a layer without changing its size by setting the layer's position property. This property accepts a CGPoint structure to identify the offset to display the layer on the screen. Unlike the frame property, the position property specifies the midpoint of the layer, not the upper-left corner:

CGPoint logoPosition = CGPointMake(75.0, 50.0);
teamLogoView.layer.position = logoPosition;

5.1.3. Arrangement and Display

In addition to adding sublayers, the CALayer class provides a number of different methods to insert, arrange, and remove layers.

When you add a layer using addSublayer, it is added to the top of the layer hierarchy, so it will display in front of any existing sublayers. Using a set of alternative methods named insertSublayer, you can slip new layers in between existing layers.

To insert a layer at a specific index, use the atIndex argument:

[ gameLayer insertSublayer: HUDView.layer atIndex: 1 ];

To insert a layer above or below another layer, add the above or below argument:

[ gameLayer insertSublayer: HUDView.layer below: backgroundView.layer ];
[ gameLayer insertSublayer: HUDView.layer above: characterView.layer ];

To remove a layer from its parent (super) layer, call the child layer's removeFromSuperlayer method:

[ HUDView.layer removeFromSuperlayer ];

To replace an existing sublayer with a different layer, use the replaceSublayer method:

[ gameLayer replaceSublayer: backgroundView.layer with: newBackgroundView.layer ];

                                          

To keep a sublayer on the stack but render it invisible, set the layer's hidden property. You can use the following code to toggle the HUD's display without actually removing the layer:

- (void) ToggleHUD {
    HUDView.layer.hidden = (HUDView.layer.hidden == NO) ? YES : NO;
}

5.1.4. Rendering

When updating a layer, the changes are not immediately rendered on the screen. This allows you to make a number of writes to layers privately, without showing the user until all operations have completed. When the layer is ready to be redrawn, invoke the layer's setNeedsDisplay method:

[ gameLayer setNeedsDisplay ];

Sometimes, it may be necessary only to redraw a part of the layer's contents. Redrawing the entire screen can slow down performance. To redraw only the portion of the screen that needs updating, use the setNeedsDisplayInRect method, passing in a CGRect structure of the region to update:

CGRect teamLogoFrame = CGRectMake(150.0, 100.0, 50.0, 75.0);
[ gameLayer setNeedsDisplayInRect: teamLogoFrame ];

If you're using the Core Graphics framework to perform rendering, you can render directly into a Core Graphics context. Use the renderInContext method to do this:

CGContextRef myCGContext = UIGraphicsGetCurrentContext();
[ gameLayer renderInContext: myCGContext ];

5.1.5. Transformations

To add a 3D or affine transformation to a layer, set the layer's transform or affineTransform properties, respectively. You'll learn more about transformations later in the chapter:

characterView.layer.transform = CATransform3DMakeScale(-1.0, -1.0, 1.0);

CGAffineTransform transform = CGAffineTransformMakeRotation(45.0);
backgroundView.layer.affineTransform = transform;

5.1.6. Layer Animations

Quartz Core's capabilities extend far beyond a mere sticky layer. It can be used to transform a 2D object into a stunning 3D texture that can be used to create beautiful transitions.

Chapter 3 introduced transitions as a means of transitioning between different UIView objects. You apply transitions and animations directly to layers or sublayers. As you recall from Chapter 3, animations are created as CATransition objects.

Layer transitions augment the existing CATransition class by providing a way to add animations using Quartz Core's animation engine. This allows the developer to take advantage of the 3D capabilities offered by Quartz Core without making significant changes to the code. When a layer is to be animated, a CATransition or CAAnimation object is attached to the layer. The layer then invokes Quartz Core to spawn a new thread that takes over all of the graphics processing for the animation. The developer needs only to add the desired animation to enhance an existing layer.

You can create a transition using the following code example:

CATransition *animation = [[CATransition alloc] init];
animation.duration = 1.0;
animation.timingFunction = [ CAMediaTimingFunction
    functionWithName: kCAMediaTimingFunctionEaseIn ];
animation.type = kCATransitionPush;
animation.subtype = kCATransitionFromRight;

At the moment, the iPhone SDK is extremely limited in the types of transitions the user may create. About a dozen additional transitions, such as page curling and zooming transitions, are supported internally by the Quartz Core framework, but are restricted to use only in Apple's own applications.


You can create an animation by creating a CABasicAnimation object. The following example creates an animation that rotates the layer a full 360 degrees:

CABasicAnimation *animation = [ CABasicAnimation
    animationWithKeyPath: @"transform" ];
animation.toValue = [ NSValue valueWithCATransform3D: CATransform3D
MakeRotation(3.1415, 0, 0, 1.0) ];
animation.duration = 2;
animation.cumulative = YES;
animation.repeatCount = 2;

Once created, you can apply the animation or transition directly to a layer:

[ teamLogoView.layer addAnimation: animation forKey: @"animation" ];

5.1.7. Layer Transformations

Quartz Core's rendering capabilities allow a 2D image to be freely manipulated as if it were 3D. An image can be rotated on an x-y-z axis to any angle, scaled, and skewed. The CATransform3D suite of functions provides the magic behind Apple's Cover Flow technology (covered in Chapter 12) and other aesthetic effects used on the iPhone. The iPhone supports scale, rotation, affine, translation transformations, and others. An introduction to layer transformations is provided in this book, but Core Animation is a big animal, and there are many books on the subject.

NOTE

 

For more information about creating animations, see Core Animation for Mac OS X and the iPhone by Bill Dudney (Pragmatic).

A transformation is executed on individual layers, allowing for multiple transformations to occur concurrently on a multilayer surface. The Quartz Core framework performs transformations using a CATransform3D object. This object is applied to a view's layer to bend or otherwise manipulate its layer into the desired 3D configuration. The application continues to treat the object as if it's a 2D object, but when it is presented to the user, the object conforms to whatever transformation has been applied to the layer. The following example creates a transformation for the purpose of rotating a layer:

CATransform3D myTransform;
myTransform = CATransform3DMakeRotation(angle, x, y, z);

The CATransform3DMakeRotation function creates a transformation that will rotate a layer by angle radians using an axis of x-y-z. The values for x-y-z define the axis and magnitude for each space (between -1 and +1). Assigning a value to an axis instructs the transformation to rotate using that axis. For example, if the x-axis is set to either -1 or +1, the object will be rotated on the x-axis in that direction, meaning it will be rotated vertically. Think of these values as inserting straws through the image for each axis. If a straw were inserted across the x-axis, the image would spin around the straw vertically. You can create more complex rotations using axis angle values. For most uses, however, values of -1 and +1 are sufficient.

To rotate a layer by 45 degrees on its horizontal axis (rotating vertically), you might use the following:

myTransform = CATransform3DMakeRotation(0.78, 1.0, 0.0, 0.0);

To rotate the same amount horizontally, specify a value for the y-axis instead:

myTransform = CATransform3DMakeRotation(0.78, 0.0, 1.0, 0.0);

The value 0.78, used in the previous example, is calculated as the radian value of the angle. To calculate radians from degrees, use the simple formula Mπ/180. For example, 45π/180 = 45(3.1415) / 180 = 0.7853. If you plan on working with the degrees measurement throughout your application, you can write a simple conversion function to help keep your code understandable:

double radians(float degrees) {
    return ( degrees * 3.14159265 ) / 180.0;
}

You'll then be able to call this function when creating transformations:

myTransform = CATransform3DMakeRotation(radians(45.0), 0.0, 1.0, 0.0);

Once the transformation has been created, apply it to the layer you're operating on. The CALayer object provides a transform property used to attach the transformation. The layer will execute whatever transformation is assigned to this property:

imageView.layer.transform = myTransform;

When the object is displayed, it will be displayed with the transformation applied to it. You will still refer to the object as a 2D object in your code, but it will be rendered according to the transformation.

5.1.8. BounceDemo: Layer Fun

This example creates two layers out of images downloaded from the Internet. It then attaches those layers to a view controller, where they can be easily manipulated later by means of a timer. The timer adjusts the on-screen position of each layer and adds an animation. This code should serve as a good, functional primer for building your own layering routines, as shown in Figure 5-1.

Figure 5-1. BounceDemo example


You can compile this application, shown in Examples Example 5-1 through Example 5-5, with the SDK by creating a view-based application project named BounceDemo. Be sure to pull out the Interface Builder code if you'd like to see how these objects are created from scratch.

Example 5-1. BouceDemo application delegate prototypes (BounceDemoAppDelegate.h)
#import <UIKit/UIKit.h>

@class BounceDemoViewController;

@interface BounceDemoAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    BounceDemoViewController *viewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet BounceDemoViewController *viewController;

@end

                                          

Example 5-2. BouceDemo application delegate (BounceDemoAppDelegate.m)
#import "BounceDemoAppDelegate.h"
#import "BounceDemoViewController.h"

@implementation BounceDemoAppDelegate

@synthesize window;
@synthesize viewController;


- (void)applicationDidFinishLaunching:(UIApplication *)application {
    CGRect screenBounds = [ [ UIScreen mainScreen ] applicationFrame ];

    self.window = [ [ [ UIWindow alloc ] initWithFrame: screenBounds ]
        autorelease
    ];

    viewController = [ [ BounceDemoViewController alloc ] init ];

    [ window addSubview: viewController.view ];
    [ window makeKeyAndVisible ];
}

- (void)dealloc {
    [ viewController release ];
    [ window release ];
    [ super dealloc ];
}

@end

Example 5-3. BouceDemo view controller prototypes (BounceDemoViewController.h)
#import <UIKit/UIKit.h>

@interface BounceDemoViewController : UIViewController {
    UIImageView *image1, *image2;
    CGPoint directionImage1, directionImage2;
    NSTimer *timer;
}
- (void) handleTimer: (NSTimer *) timer;
@end

Example 5-4. BouceDemo view controller (BounceDemoViewController.m)
#import "BounceDemoViewController.h"
#import <QuartzCore/QuartzCore.h>

@implementation BounceDemoViewController

- (id)init {
    self = [ super init ];
    if (self != nil) {
        UIImage *image;
        NSURL *url;

        url = [ NSURL URLWithString:
               @"http://www.zdziarski.com/demo/1.png" ];
        image = [ UIImage imageWithData: [ NSData dataWithContentsOfURL: url ] ];
        image1 = [ [ UIImageView alloc ] initWithImage: image ];
        directionImage1 = CGPointMake(-1.0, -1.0);
        image1.layer.position = CGPointMake((image.size.width / 2)+1,
                                          (image.size.width / 2)+1);

        url = [ NSURL URLWithString: @"http://www.zdziarski.com/demo/2s.png" ];
        image = [ UIImage imageWithData: [ NSData dataWithContentsOfURL: url ] ];
        image2 = [ [ UIImageView alloc ] initWithImage: image ];
        directionImage2 = CGPointMake(1.0, 1.0);
        image2.layer.position = CGPointMake((image.size.width / 2)+1,
                                          (image.size.width / 2)+1);

        [ self.view.layer addSublayer: image2.layer ];
        [ self.view.layer addSublayer: image1.layer ];
    }
    return self;
}

- (void)loadView {
    [ super loadView ];
}

- (void)viewDidLoad {
    [ super viewDidLoad ];

    timer = [ NSTimer scheduledTimerWithTimeInterval: 0.01
                                              target: self
                                            selector: @selector(handleTimer:)
                                            userInfo: nil
                                             repeats: YES
             ];

    CABasicAnimation *anim1 =
        [ CABasicAnimation animationWithKeyPath: @"transform" ];
    anim1.toValue = [ NSValue valueWithCATransform3D:
        CATransform3DMakeRotation(3.1415, 1.0, 0, 0)
    ];

    anim1.duration = 2;
    anim1.cumulative = YES;
    anim1.repeatCount = 1000;
    [ image1.layer addAnimation: anim1 forKey: @"transformAnimation" ];
}

- (void)didReceiveMemoryWarning {
    [ super didReceiveMemoryWarning ];
}

- (void)dealloc {
    [ timer invalidate ];
    [ image1 release ];
    [ image2 release ];
    [ super dealloc ];
}

- (void) handleTimer: (NSTimer *) timer {
    CGSize size;
    CGPoint origin;

    /* Move the image1 */
    size = [ image1 image ].size;
    if (image1.layer.position.x <=
        ( (size.width / 2) + 1) - self.view.frame.origin.x)
        directionImage1.x = 1.0;
    if (image1.layer.position.x + (size.width / 2) + 1 >=
        (self.view.frame.size.width - self.view.frame.origin.x) - 1)
        directionImage1.x = -1.0;
    if (image1.layer.position.y <=
        ( (size.height / 2) + 1) - self.view.frame.origin.y)
        directionImage1.y = 1.0;
    if (image1.layer.position.y + (size.height / 2) + 1 >=
        (self.view.frame.size.height - self.view.frame.origin.y) - 1)
        directionImage1.y = -1.0;
    origin = image1.layer.position;
    origin.x += directionImage1.x;
    origin.y += directionImage1.y;
    image1.layer.position = origin;

    /* Move the image2 */
    size = [ image2 image ].size;
    if (image2.layer.position.x <=
        ( (size.width / 2) + 1) - self.view.frame.origin.x)
        directionImage2.x = 1.0;
    if (image2.layer.position.x + (size.width / 2) + 1
        >= (self.view.frame.size.width - self.view.frame.origin.x) - 1)
        directionImage2.x = -1.0;
    if (image2.layer.position.y <=
        ( (size.height / 2) + 1) - self.view.frame.origin.y)
        directionImage2.y = 1.0;
    if (image2.layer.position.y + (size.height / 2) + 1 >=
        (self.view.frame.size.height - self.view.frame.origin.y) - 1)
        directionImage2.y = -1.0;
    origin = image2.layer.position;
    origin.x += directionImage2.x;
    origin.y += directionImage2.y;
    image2.layer.position = origin;

    [ self.view setNeedsDisplayInRect: image1.layer.frame ];
    [ self.view setNeedsDisplayInRect: image2.layer.frame ];
}
@end

                                          

Example 5-5. BouceDemo main (main.m)
#import <UIKit/UIKit.h>

int main(int argc, char *argv[]) {

    NSAutoreleasePool * pool = [ [ NSAutoreleasePool alloc ] init ];
    int retVal = UIApplicationMain(argc, argv, nil, @"BounceDemoAppDelegate");
    [ pool release ];
    return retVal;
}

                                          

5.1.9. What's Going On

Here's how the bounce demo works:

  1. When the application loads, its BounceDemoAppDelegate class is notified through its applicationDidFinishLaunching method. This method constructs the window and instantiates a view controller.

  2. The view controller's init method creates two UIImageView objects by loading images from the Internet. The layers for these view objects are added as sublayers to the view controller's main view.

  3. When the view loads, the viewDidLoad method is invoked, which creates a timer. The timer's target, handleTimer, adjusts the position of each layer by one pixel every time it is called, to give the appearance that the images are bouncing around the screen. The viewDidLoad method also creates two animations: one to flip a layer vertically and another to flip it horizontally. The animations are added to the target objects and run automatically.

5.1.10. Further Study

Now that you have an understanding of layer manipulation and animations, try a couple of things before moving on.

  • Read up on Core Animation on the Apple Developer Connection website. Change the animations in the project to apply fades, resizes, and other animations.

  • Check out the following prototypes in your SDK's header files: CALayer.h, CAAnimation.h, and CATransform3D.h. You'll find these deep within /Developer/Platforms/iPhoneOS.platform, inside the Quartz Core framework's Headers directory.