13.1. PageControl: Page Flicking Example

This example (shown in Figure 13-1) implements a custom class named PageScrollView to display five pages of content that you can thumb through by scrolling left or right. The PageScrollView class provided in this example is entirely self-contained. It accepts an NSMutableArray containing a UIView class-derived object for each page. The class then makes room for a UIPageControl and automatically updates it as the user flicks with his thumb. The user can also tap the page control on either the left or right to change pages. As is the case with all examples in this book, the PageScrollView class doesn't break any SDK rules and uses only the sanctioned interfaces available. It also illustrates how a protocol implementation works, by means of the PageScrollViewDe‚Ā†legate protocol, which notifies its delegate of page changes. Because the UIScrollView class handles all of the animation work of scrolling, you won't need to use Core Animation here at all.

Figure 13-1. PageControl example


You can compile this application, shown in Examples Example 13-1 through Example 13-7, with the SDK by creating a view-based application project named PageControl. In addition to this, you'll need to add two new files to your project: PageScrollView.h and PageScrollView.m. You can do this by selecting New File from Xcode's File menu, then selecting Objective-C class from the Cocoa template group underneath Mac OS X.

Be sure to pull out the Interface Builder code so you can see how these objects are created from scratch.

Example 13-1. PageControl application delegate prototypes (PageControlAppDelegate.h)
#import <UIKit/UIKit.h>

@class PageControlViewController;

@interface PageControlAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    PageControlViewController *viewController;
}

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

@end

                                          

Example 13-2. PageControl application delegate (PageControlAppDelegate.m)
#import "PageControlAppDelegate.h"
#import "PageControlViewController.h"

@implementation PageControlAppDelegate

@synthesize window;
@synthesize viewController;


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

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

    viewController = [ [ PageControlViewController alloc ] init ];

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

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

Example 13-3. PageControl PageScrollView class prototypes (PageScrollView.h)
#import <UIKit/UIKit.h>

@interface PageScrollView : UIView <UIScrollViewDelegate> {
    UIScrollView *scrollView;
    UIPageControl *pageControl;

    CGRect _pageRegion, _controlRegion;
    NSMutableArray *_pages;
    id _delegate;
}
-(void)layoutViews;
-(void) notifyPageChange;

@property(nonatomic,assign,getter=getPages)       NSMutableArray * pages;
@property(nonatomic,assign,getter=getCurrentPage) int              currentPage;
@property(nonatomic,assign,getter=getDelegate)    id               delegate;
@end

@protocol PageScrollViewDelegate<NSObject>

@optional

-(void) pageScrollViewDidChangeCurrentPage:
    (PageScrollView *)pageScrollView currentPage:(int)currentPage;
@end

                                          

Example 13-4. PageControl PageScrollView class (PageScrollView.m)
#import "PageScrollView.h"

@implementation PageScrollView

-(id)initWithFrame:(CGRect)frame {
    self = [ super initWithFrame: frame ];
    if (self != nil) {
        _pages = nil;
        _pageRegion = CGRectMake(frame.origin.x, frame.origin.y,
             frame.size.width, frame.size.height - 60.0);
        _controlRegion = CGRectMake(frame.origin.x, frame.size.height - 60.0,
             frame.size.width, 60.0);
        self.delegate = nil;

        scrollView = [ [ UIScrollView alloc ] initWithFrame: _pageRegion ];
        scrollView.pagingEnabled = YES;
        scrollView.delegate = self;
        [ self addSubview: scrollView ];

        pageControl = [ [ UIPageControl alloc ] initWithFrame: _controlRegion ];
        [ pageControl addTarget: self action: @selector(pageControlDidChange:)
            forControlEvents: UIControlEventValueChanged ];
        [ self addSubview: pageControl ];
    }
    return self;
}

-(void)setPages:(NSMutableArray *)pages {
    if (_pages != nil) {
        for(int i=0;i<[_pages count];i++) {
            [ [ _pages objectAtIndex: i ] removeFromSuperview ];
        }
    }
    _pages = pages;
    scrollView.contentOffset = CGPointMake(0.0, 0.0);
    scrollView.contentSize = CGSizeMake(_pageRegion.size.width * [ _pages count ],
        _pageRegion.size.height);
    pageControl.numberOfPages = [ _pages count ];
    pageControl.currentPage = 0;
    [ self layoutViews ];
}

- (void)layoutViews {
    for(int i=0;i<[ _pages count];i++) {
        UIView *page = [ _pages objectAtIndex: i ];
        CGRect bounds = page.bounds;
        CGRect frame = CGRectMake(_pageRegion.size.width * i, 0.0,
            _pageRegion.size.width, _pageRegion.size.height);
        page.frame = frame;
        page.bounds = bounds;
        [ scrollView addSubview: page ];
    }
}

-(id)getDelegate {
    return _delegate;
}

- (void)setDelegate:(id)delegate {
    _delegate = delegate;
}

-(NSMutableArray *)getPages {
    return _pages;
}

-(void)setCurrentPage:(int)page {
    [ scrollView setContentOffset:
        CGPointMake(_pageRegion.size.width * page, scrollView.contentOffset.y)
      animated: YES
    ];
    pageControl.currentPage = page;
}

-(int)getCurrentPage {
    return (int) (scrollView.contentOffset.x / _pageRegion.size.width);
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    pageControl.currentPage = self.currentPage;
    [ self notifyPageChange ];
}

-(void) pageControlDidChange: (id)sender
{
    UIPageControl *control = (UIPageControl *) sender;
    if (control == pageControl) {
        self.currentPage = control.currentPage;
    }
    [ self notifyPageChange ];
}

-(void) notifyPageChange {
    if (self.delegate != nil) {
        if ([ _delegate conformsToProtocol:@protocol(PageScrollViewDelegate) ]) {
            if ([ _delegate respondsToSelector:
                @selector(pageScrollViewDidChangeCurrentPage:currentPage:) ])
            {
                [ self.delegate pageScrollViewDidChangeCurrentPage:
                    (PageScrollView *)self currentPage: self.currentPage
                ];
            }
        }
    }
}

@end

                                          

Example 13-5. PageControl view controller prototypes (PageControlViewController.h)
#import <UIKit/UIKit.h>
#import "PageScrollView.h"

@interface PageControlViewController : UIViewController <PageScrollViewDelegate> {
    NSMutableArray *pages;
    PageScrollView *scrollView;
}
-(void) pageScrollViewDidChangeCurrentPage:(PageScrollView *)pageScrollView
 currentPage:(int)currentPage;

@end

                                          

Example 13-6. PageControl view controller (PageControlViewController.m)
#import "PageControlViewController.h"

@implementation PageControlViewController

- (void)loadView {
    [ super loadView ];

    /* Load demo images for pages */
    pages = [ [ NSMutableArray alloc ] init ];
    for(int i = 0; i < 5; i++) {
        NSLog(@"Loading demo image %d\n", i);

        UIImage *background = [ [ UIImage alloc ] initWithData:
            [ NSData dataWithContentsOfURL: [ NSURL URLWithString:
              @"http://www.zdziarski.com/demo/black.png" ] ]
        ];

        UIImage *image = [ [ UIImage alloc ] initWithData:
            [ NSData dataWithContentsOfURL: [ NSURL URLWithString:
            [ NSString stringWithFormat:
                @"http://www.zdziarski.com/demo/%d.png", i+1 ] ] ]
        ];

        UIImageView *page = [ [ UIImageView alloc ] initWithFrame: [
            [ UIScreen mainScreen ] applicationFrame ]
        ];
        page.image = background;

        UIImageView *subview = [ [ UIImageView alloc ] initWithFrame: [
            [ UIScreen mainScreen ] applicationFrame ]
        ];
        subview.image = image;
        subview.bounds = CGRectMake(0.0, 0.0, image.size.width, image.size.height);

        [ page addSubview: subview ];
        [ pages addObject: page ];
    }

    scrollView = [ [ PageScrollView alloc ] initWithFrame: self.view.frame ];
    scrollView.pages = pages;
    scrollView.delegate = self;
    self.view = scrollView;
}

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

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

-(void) pageScrollViewDidChangeCurrentPage:
    (PageScrollView *)pageScrollView currentPage:(int)currentPage
{
    NSLog(@"I'm now on page %d\n", currentPage);
}
@end

                                          

Example 13-7. PageControl main (main.m)
#import <UIKit/UIKit.h>

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

                                          

13.1.1. What's Going On?

Here's how the PageControl 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 PageScrollView class, included in the example. It then downloads five demo images from a website and builds an array of UIImageView objects. The PageScrollView class will accept any derivative of the UIView class. This array is assigned to the PageScrollView class's pages property.

  3. The PageScrollView class initializes a UIScrollView that is 1,600 pixels wide (320 x 5 pages). Each page is added with an x-origin having a multiple of 320 pixels (the width of the screen). The scroller's paging is enabled to automatically snap to a single page. A UIPageControl is added to the bottom of the screen, and the first page in the scroll view is displayed.

  4. When the user scrolls or taps the page control, the new page scrolls into view and the delegate is notified of the page change.

  5. If the pages property is set again, the scroll view reconfigures itself to the new set of pages.

13.1.2. Further Study

Scroll views are very versatile tools, and provide some great advanced functionality, as you've just learned. Try these exercises while you're in the code.

  • Reconfigure the scroll view so that it scrolls pages vertically rather than horizontally.

  • If you haven't already done so, check out the following prototypes in your SDK's header files: UIScrollView.h, UIView.h, and UIPageControl.h. You'll find these deep within /Developer/Platforms/iPhoneOS.platform, inside the UI Kit framework's Headers directory.