Using UIPageControl as a container UIViewController

Setting up the interface

To setup the user interface in Interface Builder, you’ll need to create a UIPageControl and a UIScrollView within a UIViewController, and as many other UIViewControllers as to need.

Setting up the PageViewController class

The PageViewController will contain 2 subview a UIScrollView and a UIPageControl. The UIScrollView will contain the views while the UIPageControl will navigate and control which part of the UIScrollView is visible.
You’ll also need some mechanism for calling addChildViewController: on the view controller, I’m my example I did this by sub-classing PagerViewController, but you could also do this before the view controller is push into view.

So, the public interface should look like this:

@interface PagerViewController : UIViewController 
 
@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
@property (nonatomic, strong) IBOutlet UIPageControl *pageControl;
 
- (IBAction)changePage:(id)sender;
 
@end

On to the implementation file. First of all, I want to handle all the view appearence at rotation calls myself so, we need to over-ride automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers and return NO indicating we don’t want the UIViewController super class doing this.
When the PagerViewController view comes into view, we will need to signal to the currently active child view controller that it has become visible. So we will forward on the view appeared/disappeared messages.

- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers {
	return NO;
}
 
- (void)viewDidAppear:(BOOL)animated {
	[super viewDidAppear:animated];
	UIViewController *viewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
	if (viewController.view.superview != nil) {
		[viewController viewDidAppear:animated];
	}
}
 
- (void)viewWillDisappear:(BOOL)animated {
	UIViewController *viewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
	if (viewController.view.superview != nil) {
		[viewController viewWillDisappear:animated];
	}
	[super viewWillDisappear:animated];
}
 
- (void)viewDidDisappear:(BOOL)animated {
	UIViewController *viewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
	if (viewController.view.superview != nil) {
		[viewController viewDidDisappear:animated];
	}
	[super viewDidDisappear:animated];
}

viewWillAppear is a little more complex since we also want to load all the child view controllers into the scroll view (we’ll cover loadScrollViewWithPage next), and also make sure the scroll view’s content size is large enough to handle all the child views.

- (void)viewWillAppear:(BOOL)animated {
	[super viewWillAppear:animated];
 
	for (NSUInteger i =0; i < [self.childViewControllers count]; i++) {
		[self loadScrollViewWithPage:i];
	}
 
	self.pageControl.currentPage = 0;
	_page = 0;
	[self.pageControl setNumberOfPages:[self.childViewControllers count]];
 
	UIViewController *viewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
	if (viewController.view.superview != nil) {
		[viewController viewWillAppear:animated];
	}
 
	self.scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * [self.childViewControllers count], scrollView.frame.size.height);
}

To load the content of the UIViewController into the UIScrolView contentView we go though each child and add its view as a subview of the UIScrollView off-setting the origin by a screen width each time.

- (void)loadScrollViewWithPage:(int)page {
    if (page < 0)
        return;
    if (page >= [self.childViewControllers count])
        return;
 
	// replace the placeholder if necessary
    UIViewController *controller = [self.childViewControllers objectAtIndex:page];
    if (controller == nil) {
		return;
    }
 
	// add the controller's view to the scroll view
    if (controller.view.superview == nil) {
        CGRect frame = self.scrollView.frame;
        frame.origin.x = frame.size.width * page;
        frame.origin.y = 0;
        controller.view.frame = frame;
        [self.scrollView addSubview:controller.view];
    }
}

Handling scrolling

To handle scrolling we need to implement a few UIScrollViewDelegate methods and also the - (IBAction)changePage:(id)sender method we declared earlier.
We need to know how the scrolling occurred, i.e. was it initiated from a gesture swipe across the screen, or by tapping either side of the UIPageControl.

To work this out we update an ivar when the various delegate callback are called..
This code is largely taken from this Cocoa with Love post
I also call the child UIViewController’s viewillAppear etc. methods.

// At the begin of scroll dragging, reset the boolean used when scrolls originate from the UIPageControl
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
         _pageControlUsed = NO;
}
 
// At the end of scroll animation, reset the boolean used when scrolls originate from the UIPageControl
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
         _pageControlUsed = NO;
}
 
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
        UIViewController *oldViewController = [self.childViewControllers objectAtIndex:_page];
        UIViewController *newViewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
        [oldViewController viewDidDisappear:YES];
        [newViewController viewDidAppear:YES];
 
        _page = self.pageControl.currentPage;
}

Now, to update the display after the page change we just need implement the scrollViewDidScroll delegate method and the changePage IBAction method.
changing the viewable UIViewController is done by simply scrolling the UIScrollView to the appropriate location.

- (IBAction)changePage:(id)sender {
        int page = ((UIPageControl *)sender).currentPage;
 
        // update the scroll view to the appropriate page
        CGRect frame = self.scrollView.frame;
        frame.origin.x = frame.size.width * page;
        frame.origin.y = 0;
 
        UIViewController *oldViewController = [self.childViewControllers objectAtIndex:_page];
        UIViewController *newViewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
        [oldViewController viewWillDisappear:YES];
        [newViewController viewWillAppear:YES];
 
        [self.scrollView scrollRectToVisible:frame animated:YES];
 
        // Set the boolean used when scrolls originate from the UIPageControl. See scrollViewDidScroll: above.
        _pageControlUsed = YES;
}
 
- (void)scrollViewDidScroll:(UIScrollView *)sender {
    // We don't want a "feedback loop" between the UIPageControl and the scroll delegate in
    // which a scroll event generated from the user hitting the page control triggers updates from
    // the delegate method. We use a boolean to disable the delegate logic when the page control is used.
    if (_pageControlUsed || _rotating) {
        // do nothing - the scroll was initiated from the page control, not the user dragging
        return;
    }
 
    // Switch the indicator when more than 50% of the previous/next page is visible
        CGFloat pageWidth = self.scrollView.frame.size.width;
        int page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
        if (self.pageControl.currentPage != page) {
                UIViewController *oldViewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
                UIViewController *newViewController = [self.childViewControllers objectAtIndex:page];
                [oldViewController viewWillDisappear:YES];
                [newViewController viewWillAppear:YES];
                self.pageControl.currentPage = page;
                [oldViewController viewDidDisappear:YES];
                [newViewController viewDidAppear:YES];
                _page = page;
        }
}

Finally, Rotation

To handle device rotation, we need to return YES from shouldAutorotateToInterfaceOrientation.
We also need to pass on the following messages to the currently active child UIViewController.

  • willAnimateRotationToInterfaceOrientation:duration:
  • willRotateToInterfaceOrientation:duration:
  • didRotateFromInterfaceOrientation:

But, we also need to handle our own rotation, i.e. resizing the scrollviews contentView, and adjusting the frame of the child subviews, otherwise everything is miss-aligned. This is done inwillAnimateRotationToInterfaceOrientation:duration:, so that the resizing is also animated.

When the frame of UIScrollView adjusts scrollViewDidScroll: also gets called, so to prevent that from flipping us to a different page we set and unset the _rotating flag in the following methods:

  • willRotateToInterfaceOrientation:duration:
  • didRotateFromInterfaceOrientation:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
	return YES;
}
 
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
	UIViewController *viewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
	[viewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
	_rotating = YES;
}
 
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
 
	UIViewController *viewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
	[viewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
 
	self.scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * [self.childViewControllers count], scrollView.frame.size.height);
	NSUInteger page = 0;
	for (viewController in self.childViewControllers) {
		CGRect frame = self.scrollView.frame;
		frame.origin.x = frame.size.width * page;
		frame.origin.y = 0;
		viewController.view.frame = frame;
		page++;
	}
 
	CGRect frame = self.scrollView.frame;
	frame.origin.x = frame.size.width * _page;
	frame.origin.y = 0;
	[self.scrollView scrollRectToVisible:frame animated:NO];
 
}
 
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
	_rotating = NO;
	UIViewController *viewController = [self.childViewControllers objectAtIndex:self.pageControl.currentPage];
	[viewController didRotateFromInterfaceOrientation:fromInterfaceOrientation];
}

Conclusion

 

For fully working example project see this repository on GitHub
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s