12.1. CovertFlow: SDK Cover Flow Programming

In this example, you'll create a subclass of the UIScrollView class named CFView class. The CFView class is initialized with a set of images representing your album covers (or whatever content you want to present). The class uses Core Animation to create a Cover Flow-style layout on the iPhone's screen. As you scroll with your finger, the underlying UIScrollView class invokes its delegate's scrollViewDidScroll method. This method calculates which album cover is at the center position of the screen and rotates it into view. See Figure 12-1.

Figure 12-1. CovertFlow example


The CFView class was designed with reusability in mind. To implement this into your own code, look at how the view controller class initializes it and reads its selectedCover property.

You can compile this application, shown in Examples Example 12-1 through Example 12-5, with the SDK by creating a view-based application project named CovertFlow (note the name "Covert" with a "t"). You'll need to add the Quartz Core framework to your project in order to compile it. Be sure to pull out the Interface Builder code so you can see how these objects are created from scratch.

Example 12-1. CovertFlow application delegate prototypes (CovertFlowAppDelegate.h)
#import <UIKit/UIKit.h>

@class CovertFlowViewController;

@interface CovertFlowAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    CovertFlowViewController *viewController;
}

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

@end

                                          

Example 12-2. CovertFlow application delegate (CovertFlowAppDelegate.m)
#import "CovertFlowAppDelegate.h"
#import "CovertFlowViewController.h"

@implementation CovertFlowAppDelegate

@synthesize window;
@synthesize viewController;


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

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

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


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


@end

                                          

Example 12-3. CovertFlow view controller prototypes (CovertFlowViewController.h)
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

/* Number of pixels scrolled before next cover comes front */
#define SCROLL_PIXELS 60.0

/* Size of each cover */
#define COVER_WIDTH_HEIGHT 128.0

@interface CFView : UIScrollView <UIScrollViewDelegate>
{
    CAScrollLayer *cfIntLayer;
    NSMutableArray *_covers;
    NSTimer *timer;
    int selectedCover;
}
- (id) initWithFrame:(struct CGRect)frame covers:(NSMutableArray *)covers;
- (void)layoutLayer:(CAScrollLayer *)layer;

@property(nonatomic,getter=getSelectedCover) int selectedCover;

@end

@interface CovertFlowViewController : UIViewController {
    NSMutableArray *covers;
    CFView *covertFlowView;
}


@end

                                          

Example 12-4. CovertFlow view controller (CovertFlowViewController.m)
#import "CovertFlowViewController.h"

@implementation CFView

- (id) initWithFrame:(struct CGRect)frame covers:(NSMutableArray *)covers {
    self = [ super initWithFrame: frame ];

    if (self != nil) {
        _covers = covers;
        selectedCover = 0;

        self.showsVerticalScrollIndicator = YES;
        self.showsHorizontalScrollIndicator = NO;
        self.delegate = self;
        self.scrollsToTop = NO;
        self.bouncesZoom = NO;

        cfIntLayer = [ [ CAScrollLayer alloc ] init ];
        cfIntLayer.bounds = CGRectMake(0.0, 0.0, frame.size.width,
             frame.size.height + COVER_WIDTH_HEIGHT);
        cfIntLayer.position = CGPointMake(160.0, 304.0);
        cfIntLayer.frame = frame;

        for(int i = 0; i < [ _covers count ]; i++) {
            NSLog(@"Initializing cfIntLayer layer %d\n", i);
            UIImageView *background = [ [ [ UIImageView alloc ] initWithImage:
                [ _covers objectAtIndex: i ] ] autorelease ];
            background.frame = CGRectMake(0.0, 0.0, COVER_WIDTH_HEIGHT,
                 COVER_WIDTH_HEIGHT);
            [ cfIntLayer addSublayer: background.layer ];
        }

        self.contentSize = CGSizeMake(320.0, ( ( frame.size.height) +
             (SCROLL_PIXELS * ([ _covers count ] -1)) ) );

        [ self.layer addSublayer: cfIntLayer ];
        [ self layoutLayer: cfIntLayer ];
    }

    return self;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    selectedCover = (int) roundf((self.contentOffset.y/SCROLL_PIXELS));
    if (selectedCover > [ _covers count ] -1) {
        selectedCover = [ _covers count ] - 1;
    }
    [ self layoutLayer: cfIntLayer ];
}

- (void)setSelectedCover:(int)index {

    if (index != selectedCover) {
        selectedCover  = index;
        [ self layoutLayer: cfIntLayer ];
        self.contentOffset = CGPointMake(self.contentOffset.x, selectedCover *
             SCROLL_PIXELS);
    }
}

- (int) getSelectedCover {
    return selectedCover;
}

-(void) layoutLayer:(CAScrollLayer *)layer
{
    CALayer *sublayer;
    NSArray *array;
    size_t i, count;
    CGRect rect, cfImageRect;
    CGSize cellSize, spacing, margin, size;
    CATransform3D leftTransform, rightTransform, sublayerTransform;
    float zCenterPosition, zSidePosition;
    float sideSpacingFactor, rowScaleFactor;
    float angle = 1.39;
    int x;

    size = [ layer bounds ].size;

    zCenterPosition = 60;      /* Z-Position of selected cover */
    zSidePosition = 0;         /* Default Z-Position for other covers */
    sideSpacingFactor = .85;   /* How close should slide covers be */
    rowScaleFactor = .55;      /* Distance between main cover and side covers */

    leftTransform = CATransform3DMakeRotation(angle, -1, 0, 0);
    rightTransform = CATransform3DMakeRotation(-angle, -1, 0, 0);

    margin   = CGSizeMake(5.0, 5.0);
    spacing  = CGSizeMake(5.0, 5.0);
    cellSize = CGSizeMake (COVER_WIDTH_HEIGHT, COVER_WIDTH_HEIGHT);

    margin.width += (size.width - cellSize.width * [ _covers count ]
                     -  spacing.width * ([ _covers count ] - 1)) * .5;
    margin.width = floor (margin.width);

    /* Build an array of covers */
    array = [ layer sublayers ];
    count = [ array count ];
    sublayerTransform = CATransform3DIdentity;

    /* Set perspective */
    sublayerTransform.m34 = -0.006;

    /* Begin a CATransaction so that all animations happen simultaneously */
    [ CATransaction begin ];
    [ CATransaction setValue: [ NSNumber numberWithFloat: 0.3f ]
                      forKey:@"animationDuration" ];

    for (i = 0; i < count; i++)
    {
        sublayer = [ array objectAtIndex:i ];
        x = i;

        rect.size = *(CGSize *)&cellSize;
        rect.origin = CGPointZero;
        cfImageRect = rect;

        /* Base position */
        rect.origin.x = size.width / 2 - cellSize.width / 2;
        rect.origin.y = margin.height + x * (cellSize.height + spacing.height);

        [ [ sublayer superlayer ] setSublayerTransform: sublayerTransform ];

        if (x < selectedCover)        /* Left side */
        {
            rect.origin.y += cellSize.height * sideSpacingFactor
            * (float) (selectedCover - x - rowScaleFactor);
            sublayer.zPosition = zSidePosition - 2.0 * (selectedCover - x);
            sublayer.transform = leftTransform;
        }
        else if (x > selectedCover)   /* Right side */
        {
            rect.origin.y -= cellSize.height * sideSpacingFactor
            * (float) (x - selectedCover - rowScaleFactor);
            sublayer.zPosition = zSidePosition - 2.0 * (x - selectedCover);
            sublayer.transform = rightTransform;
        }
        else                     /* Selected cover */
        {
            sublayer.transform = CATransform3DIdentity;
            sublayer.zPosition = zCenterPosition;

            /* Position in the middle of the scroll layer */
            [ layer scrollToPoint: CGPointMake(0, rect.origin.y
                - (([ layer bounds ].size.height - cellSize.width)/2.0))
            ];

            /* Position the scroll layer in the center of the view */
            layer.position =
            CGPointMake(160.0f, 240.0f + (selectedCover * SCROLL_PIXELS));
        }
        [ sublayer setFrame: rect ];

    }
    [ CATransaction commit ];
}

@end

@implementation CovertFlowViewController

- (id)init {
    self = [ super init ];
    if (self != nil) {
        covers = [ [ NSMutableArray alloc ] init ];

        for(int i = 1; i < 6; i++) {
            NSLog(@"Loading demo image %d\n", i);
            UIImage *image = [ [ UIImage alloc ] initWithData:
                  [ NSData dataWithContentsOfURL:
                      [ NSURL URLWithString: [ NSString stringWithFormat:
                          @"http://www.zdziarski.com/demo/%d.png", i ] ] ]
              ];

            [ covers addObject: image ];
        }
    }
    return self;
}

- (void)loadView {
    [ super loadView ];

    covertFlowView = [ [ CFView alloc ] initWithFrame:
        self.view.frame
        covers: covers
    ];

    covertFlowView.selectedCover = 2;
    self.view = covertFlowView;
}


- (BOOL)shouldAutorotateToInterfaceOrientation:
    (UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}


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


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

@end

                                          

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

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

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

                                          

12.1.1. What's Going On?

Here's how the CovertFlow example works:

  1. When the application instantiates, a window and view controller are created, just as in any other view-based application.

  2. The view controller creates an instance of the CFView class, included in the example. It then downloads five demo images from a website and builds an array of UIImage objects. This array is passed to the CFView class's custom initialization method.

  3. The CFView class initializes its own properties as a UIScrollView class. It then creates a sublayer for each image it was configured with. The sublayers are transformed to either a side view or a front view.

  4. When you set the selectedCover property, the cover is transformed to a front view and the scroll view's position is set to center the selected cover.

  5. When the user scrolls with his finger, the scrollViewDidScroll method is invoked. This calculates the position of the scroll window and sets the active cover based on where the user has scrolled. The layers are then animated to flip to the correct album cover.

12.1.2. Further Study

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

  • Change this example up to automatically scroll through each cover using an NSTimer object.

  • We've intentionally left this example's skeleton using the portrait orientation. Apply what you've learned in this book to display a table of covers when the iPhone is in portrait mode, and switch to the Cover Flow view when rotated to landscape mode.

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