13.2. A PageScrollView for Many Views

In the previous example, you learned how to roll a custom UIScrollView class whose content area accommodated all of the views it displayed. This works well when dealing with only a small number of views, but if you are flipping through hundreds of photos, this will consume too much memory. The advantage to using the former approach with a small number of views is that you can display the horizontal scroll indicator, which will give the user an idea of how many objects he is scrolling through. The advantage to using the class example here is that you can display many more views.

The example below revises the PageScrollView class to use a content area the size of only three pages. As the user scrolls left or right, the pages in view are quietly swapped out with new pages. The result is a thumb scroller that looks and feels exactly like the PageControl demo, but can handle hundreds of pages. A property has also been added to hide the UIPageControl object at the bottom of the page. To do this, set the showsPa‚Ā†geControl property to NO.

The class is defined in Examples Example 13-8 and Example 13-9. To use this class with the PageControl demo, simply replace the class files with these.

Example 13-8. PageScrollView prototypes (PageScrollView.h)
#import <UIKit/UIKit.h>

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

    CGRect _pageRegion, _controlRegion;
    NSMutableArray *_pages;
    id _delegate;
    BOOL _showsPageControl;
    int _zeroPage;
}
-(void)layoutViews;
-(void)layoutScroller;
-(void)notifyPageChange;

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

@protocol PageScrollViewDelegate<NSObject>

@optional

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

@end

                                          

Example 13-9. PageScrollView (PageScrollView.m)
#import "PageScrollView.h"

@implementation PageScrollView

-(id)initWithFrame:(CGRect)frame {
    self = [ super initWithFrame: frame ];
    if (self != nil) {
        _pages = nil;
        _zeroPage = 0;
        _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);
    if ([ _pages count] < 3) {
        scrollView.contentSize = CGSizeMake(_pageRegion.size.width *
            [ _pages count ], _pageRegion.size.height);
    } else {
        scrollView.contentSize = CGSizeMake(_pageRegion.size.width * 3,
            _pageRegion.size.height);
        scrollView.showsHorizontalScrollIndicator = NO;
    }
    pageControl.numberOfPages = [ _pages count ];
    pageControl.currentPage = 0;
    [ self layoutViews ];
}

- (void)layoutViews {
    if ([ _pages count ] <= 3) {
        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 ];
        }
        return;
    }

    /* For more than 3 views, add them all hidden, layout according to page */
    for(int i=0;i<[ _pages count];i++) {
        UIView *page = [ _pages objectAtIndex: i ];
        CGRect bounds = page.bounds;
        CGRect frame = CGRectMake(0.0, 0.0, _pageRegion.size.width,
            _pageRegion.size.height);
        page.frame = frame;
        page.bounds = bounds;
        page.hidden = YES;
        [ scrollView addSubview: page ];
    }
    [ self layoutScroller ];
}

- (void)layoutScroller {
    UIView *page;
    CGRect bounds, frame;
    int pageNum = [ self getCurrentPage ];

    if ([ _pages count ] <= 3)
        return;

    NSLog(@"Laying out scroller for page %d\n", pageNum);

    /* Left boundary */
    if (pageNum == 0) {
        for(int i=0;i<3;i++) {
            page = [ _pages objectAtIndex: i ];
            bounds = page.bounds;
            frame = CGRectMake(_pageRegion.size.width * i, 0.0,
                _pageRegion.size.width, _pageRegion.size.height);
            NSLog(@"\tOffset for Page %d = %f\n", i, frame.origin.x);
            page.frame = frame;
            page.bounds = bounds;
            page.hidden = NO;
        }
        page = [ _pages objectAtIndex: 3 ];
        page.hidden = YES;
        _zeroPage = 0;
    }

    /* Right boundary */
    else if (pageNum == [ _pages count ] -1) {
        for(int i=pageNum-2;i<=pageNum;i++) {
            page = [ _pages objectAtIndex: i ];
            bounds = page.bounds;
            frame = CGRectMake(_pageRegion.size.width * (2-(pageNum-i)), 0.0,
                _pageRegion.size.width, _pageRegion.size.height);
            NSLog(@"\tOffset for Page %d = %f\n", i, frame.origin.x);
            page.frame = frame;
            page.bounds = bounds;
            page.hidden = NO;
        }
        page = [ _pages objectAtIndex: [ _pages count ]-3 ];
        page.hidden = YES;
        _zeroPage = pageNum - 2;
    }

    /* All middle pages */
    else {
        for(int i=pageNum-1; i<=pageNum+1; i++) {
            page = [ _pages objectAtIndex: i ];
            bounds = page.bounds;
            frame = CGRectMake(_pageRegion.size.width * (i-(pageNum-1)), 0.0,
                _pageRegion.size.width, _pageRegion.size.height);
            NSLog(@"\tOffset for Page %d = %f\n", i, frame.origin.x);
            page.frame = frame;
            page.bounds = bounds;
            page.hidden = NO;
        }
        for(int i=0; i< [ _pages count ]; i++) {
            if (i < pageNum-1 || i > pageNum + 1) {
                page = [ _pages objectAtIndex: i ];
                page.hidden = YES;
            }
        }
        scrollView.contentOffset = CGPointMake(_pageRegion.size.width, 0.0);
        _zeroPage = pageNum-1;
    }
}

-(id)getDelegate {
    return _delegate;
}

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

-(BOOL)getShowsPageControl {
    return _showsPageControl;
}

-(void)setShowsPageControl:(BOOL)showsPageControl {
    _showsPageControl = showsPageControl;
    if (_showsPageControl == NO) {
        _pageRegion = CGRectMake(self.frame.origin.x, self.frame.origin.y,
            self.frame.size.width, self.frame.size.height);
        pageControl.hidden = YES;
        scrollView.frame = _pageRegion;
    } else {
        _pageRegion = CGRectMake(self.frame.origin.x, self.frame.origin.y,
            self.frame.size.width, self.frame.size.height - 60.0);
        pageControl.hidden = NO;
        scrollView.frame = _pageRegion;
    }
}

-(NSMutableArray *)getPages {
    return _pages;
}

-(void)setCurrentPage:(int)page {
    [ scrollView setContentOffset: CGPointMake(0.0, 0.0) ];
    _zeroPage = page;
    [ self layoutScroller ];
    pageControl.currentPage = page;
}

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

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

-(void) pageControlDidChange: (id)sender
{
    UIPageControl *control = (UIPageControl *) sender;
    if (control == pageControl) {
        [ scrollView setContentOffset: CGPointMake
            (pageRegion.size.width * (control.currentPage - _zeroPage), 0.0)
          animated: YES
        ];
    }
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
        [ self layoutScroller ];
        [ 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

                                          

13.2.1. What's Going On?

Here's how this new and improved PageScrollView class works:

  1. When the class is instantiated, a UIScrollView class is created. If three or fewer pages are attached, the scroll view behaves like the previous example: it allocates content space in the scroll view for all pages. If more than three pages are attached, the scroll view's content space is set to the width of three pages. This allows the user to scroll left or right from a center position. All views are then added, but hidden.

  2. After the user scrolls, the layoutScroller method is called. This determines which page the user has selected based on the position of the scroll view and the page number of the leftmost page (_zeroPage). The method then resets the origin of the current page and its two nearest pages so that they are oriented correctly on the scroll view. Since the user can only see the current page, he will not see adjacent pages being swapped out.

  3. If the user is on the leftmost (first) or rightmost (last) page, the page will be oriented on one end of the scroll view so that the user can't scroll beyond it. If the user is on any other page, the page is oriented in the center of the scroll view, and the page's left and right neighbors are swapped in on either side. The zero page is then set to the page number of the leftmost page so the class can track which page the user is on.

  4. If the user taps on the page control, the page control scrolls to the position of the previous or next page. When the scrolling animation has completed, the UIScrollView class invokes the scrollViewDidEndScrollingAnimation delegate method. This calls layoutScroller, which goes through the same layout process again.