Friday, 3 February 2012

How To: UIScrollView with Paging



Creating a UIScrollView with paging is fairly straightforward — with a few caveats. In this post I’ll walk through a simple app that allows the user to scroll through multiple pages, adding a UIPageControl for further control.
Getting Started
To start, create a new XCode project. Choose “View-based Application” as this gives us a UIViewController sub-class and XIB for free.
Let’s add a UIScrollView to the view. First add an outlet for the scroll view in the view controller, and in Interface Builder drag in a UIScrollView and link it to the outlet. For now this scroll view can take up the full height and width of the parent view.
At this point our header file contains a single property:
@interface UIScrollView_PagingViewController : UIViewController {
    UIScrollView* scrollView;
}
 
@property (nonatomic, retain) IBOutlet UIScrollView* scrollView;
 
@end
The implementation is straightforward as well:
#import "UIScrollView_PagingViewController.h"
 
@implementation UIScrollView_PagingViewController
 
@synthesize scrollView;
 
- (void)viewDidLoad {
    [super viewDidLoad];
}
 
- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
 
    // Release any cached data, images, etc that aren't in use.
}
 
- (void)viewDidUnload {
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.scrollView = nil;
}
 
- (void)dealloc {
    [scrollView release];
    [super dealloc];
}
 
@end
Here’s what the XIB looks like:
Adding the scroll view
Now let’s add some content to the scroll view. Into viewDidLoad let’s add a few pages of solid colors, each one the size of the scroll view:
- (void)viewDidLoad {
    [super viewDidLoad];
 
    NSArray *colors = [NSArray arrayWithObjects:[UIColor redColor], [UIColor greenColor], [UIColor blueColor], nil];
    for (int i = 0; i < colors.count; i++) {
        CGRect frame;
        frame.origin.x = self.scrollView.frame.size.width * i;
        frame.origin.y = 0;
        frame.size = self.scrollView.frame.size;
 
        UIView *subview = [[UIView alloc] initWithFrame:frame];
        subview.backgroundColor = [colors objectAtIndex:i];
        [self.scrollView addSubview:subview];
        [subview release];
    }
 
    self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * colors.count, self.scrollView.frame.size.height);
}
For each color in our colors array, we’re creating a subview the height and width of the scroll view, located increasingly further to the right. Finally, we set the the scroll view’s content size (that’s the size of all the stuff inside the scroll view).
If you run the app at this point, you’ll see that although you can scroll between “pages”, when you let go the scroll view stays put. When we think of “paging”, that means the user is locked into concrete pages, and once they let go of the scroll view they should be locked into a specific page.
Enabling paging
Luckily, doing this is quite simple. UIScrollView has a property called “pagingEnabled” which does exactly what I described in the last paragraph. You can set this in code or in Interface Builder. Let’s set it in Interface Builder. At the same time, we can hide the scrollers as well. Generally you don’t see scrollers inside paged scroll views.
At this point, if you’re not interested in adding a page control, you’re done! However, we’ll continue with setting up a page control, which is common for a UIScrollView with paging.
Adding the UIPageControl
The UIPageControl view is generally used anywhere the user can visually choose between discrete pages or slides. Think of the home screen, Safari, Weather, and so on.
Let’s add a UIPageControl to the app. In Interface Builder, resize the scroll view to allow for some space below it for the page control. Next, drag in a UIPageControl from the library, centered below the scroll view. Resize the UIPageControl to take up the full width of the view. I’d also recommend setting the background color of the parent view to black, just so we can more clearly see the page control.
If you try running the app, everything looks right, but our page control isn’t updating as we scroll from page to page. Let’s do that next.
Hooking up the UIPageControl
Add an outlet to the view controller called “pageControl”. In Interface Builder, connect the UIPageControl to this outlet. Now we have access to the pageControl in the view controller.
If we want the page control to update as the user changes pages, we’ll need to know when the user scrolls. For that we can use the scroll view’s delegate, UIScrollViewDelegate. We can have our view controller be the delegate. So, add the UIScrollViewDelegate protocol to our view controller, and in Interface Builder, connect the scroll view’s “delegate” outlet to our view controller.
When all that’s done, the view controller’s header file should look like this:
@interface UIScrollView_PagingViewController : UIViewController  {
    UIScrollView* scrollView;
    UIPageControl* pageControl;
}
 
@property (nonatomic, retain) IBOutlet UIScrollView* scrollView;
@property (nonatomic, retain) IBOutlet UIPageControl* pageControl;
 
@end
And the XIB:
UIScrollViewDelegate gives a whole bunch of useful methods, but the one that helps us here is “scrollViewDidScroll:”. It let’s us know that the user has scrolled the content view inside the scroll view.
So when should we update the page control? If you play with one for a bit (say, the one on your iPhone’s home screen) you’ll see that the page actually changes once you’re halfway scrolled to the previous or next screen. So as the user scroll’s, we’ll need to keep an eye on how far they’ve scrolled to the right, and figure out the closest page.
Add the following method to the view controller:
- (void)scrollViewDidScroll:(UIScrollView *)sender {
    // Update the page 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;
    self.pageControl.currentPage = page;
}
The page is computed by first starting with self.scrollView.contentOffset.x, which is how far the user has scrolled. Then subtracted from that is the half the width of a page (since the page number actually changes as the user scrolls by the midpoint of a slide). Dividing by the page width and rounding down gives the page count assuming the first page is 0. Since UIPageControl expects the first page to be 1, we add 1 to this count. Setting currentPage on the page control will automatically update the UIPageControl’s UI.
Try running the app again and as you scroll between pages you should see the page control respond just as you would expect.
Responding to the UIPageControl
However, we’re only half-way there. The page control updates as the user scroll’s between pages, but we also want to be able to switch between pages by tapping on the left or right side of the page control.
We can use the “value changed” event on UIPageControl to be told when the user changes the page using the control. Add an IBAction method called “changePage” the view controller’s header file:
- (IBAction)changePage;
In Interface builder, connect this up to the “value changed” action on the page control.
The implementation of this method is fairly straightforward:
- (IBAction)changePage {
    // update the scroll view to the appropriate page
    CGRect frame;
    frame.origin.x = self.scrollView.frame.size.width * self.pageControl.currentPage;
    frame.origin.y = 0;
    frame.size = self.scrollView.frame.size;
    [self.scrollView scrollRectToVisible:frame animated:YES];
}
You can pragmatically make a UIScrollView scroll by calling scrollRectToVisible:animated. You pass it a rectangle which you’d like to be made fully visible. In this case, that’s the new page to which we want to scroll. I’d recommend always setting animated to YES so that it’s clear to the user what’s happening.
If you try the app out it should work great…with one exception. You may notice the page control “flashing” when you use it to change pages. The page control will switch to the correct page, then switch back to the old page, and finally switch to the original, correct, page. Let’s fix that.
Fixing the flashing
What’s happening here is that when we tell the scroll view to scroll with scrollRectToVisible:animated:, the scrollViewDidScroll: method still gets called. So it will revert to the old page until we scroll past 50% of the old page, after which the page control will switch back.
So what we need to do is prevent the code in scrollRectToVisible:animated: from running if the page control was used to initiate the scroll. Add an BOOL attribute called pageControlBeingUsed to the view controller, and set it to NO in viewDidLoad.
We know we can turn it to YES when the user changes the page, so add “pageControlBeingUsed = YES;” to the end of the changePage method.
But when do we set it back to NO? There are two cases: either the user interrupts our page control driven scrolling by manually initiating a scroll, or the scroll finishes. Luckily we can use UIScrollViewDelegate to be notified of both these events:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    pageControlBeingUsed = NO;
}
 
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    pageControlBeingUsed = NO;
}
This should now fix our flashing problem, and provide us with a fully complete paging UIScrollView, with UIPageControl. I’ve made all the code in this tutorial available here. Feel free to use it as a starting point in your own projects.
I hope you found this tutorial useful! If you did, please consider signing up for my newsletter to be notified of new blog posts as well as receiving periodic tips, tricks and suggestions that I don’t post on my blog.
Update: In the comments, some people have asked about placing buttons inside the scroll view, and also about setting up the scroll view using Interface Builder. I’ve added some code that includes buttons here, and a version using Interface Builder here.

0 comments:

Post a Comment

 

Copyright @ 2013 PakTechClub.