How To Use UIScrollView to Scroll and Zoom Content

Matt Galloway Matt Galloway
Learn how to use UIScrollViews for paging, zooming, scrolling, and more!

Learn how to use UIScrollViews for paging, zooming, scrolling, and more!

This is a blog post by iOS Tutorial Team member Matt Galloway, founder of SwipeStack, a mobile development team based in London, UK. You can also find me on .

UIScrollViews are one of the most useful controls in iOS. They are a great way to present content larger than a single screen, and there’s a lot of tips and tricks about using them you should know!

In this tutorial you’ll learn all about UIScrollViews, from beginning to advanced. You’ll learn:

  • How to use a scroll view to view a very large image.
  • How to keep the scroll view’s content centered while zooming.
  • How to embed a complex view hierarchy inside a UIScrollView.
  • How to use UIScrollView’s paging feature in conjunction with the UIPageControl, to allow scrolling through multiple pages of content.
  • How to make a “peeking” scroll view that gives a glimpse of the previous/next page as well as the current page.
  • And much more!

This tutorial assumes some familiarity with Objective-C and iOS programming. If you are a complete beginner, you may wish to check out some of the other tutorials on this site first.

This tutorial also assumes that you know how to use Interface Builder to add new objects to a view and connect outlets, so make sure that you do. Further, this tutorial uses a storyboard. You’ll want to get familiar with them if you aren’t already, perhaps by reading the Storyboards tutorial on this site.

Lastly, I’ll be using Xcode 4 in this tutorial, so make sure you’re fully updated to the latest version available through the Mac App Store.

Getting Started

Fire up Xcode and create a new project with the iOS\Application\Single View Application template. Enter ScrollViews for the product name, enter the company identifier you used when creating your App ID, leave the class prefix blank, set device family to iPhone, and make sure that Use Storyboards and Use Automatic Reference Counting are checked (but leave the other checkboxes unchecked):

Click Next and choose a location to save your project.

Then download the resources for this project and extract the contents of the ZIP file into a temporary folder.

Drag & drop the extracted files into the project root, and make sure that the “Copy items into destination group’s folder (if needed)” checkbox is ticked if you are dragging and dropping from a temporary folder.

Since this tutorial is going to illustrate four different things that you can do with scroll views, the project will have a tableview menu with four options from which to select. Each option will open a new view controller to show off a certain aspect of scroll views.

This is what your storyboard will look like when you’re finished:

To build the tableview menu, do the following:

  1. Open MainStoryboard.storyboard and delete the scene that’s already in there by selecting the view controller (click on it on the story board) and then deleting it.
  2. Then, add a table view controller by dragging one from the Object Library on to the story board.
  3. Now select the table you added and then click Editor\Embed In\Navigation Controller.
  4. Select the table view within the table view controller, and set the content type to Static Cells in the attributes inspector (as shown in image below).
  5. Finally, set the number of rows in the table view section (if you don’t see it, tap on the arrow next to “Table View” in the left sidebar showing the storyboard hierarchy and then select “Table View Section”) to 4, and for each row in the table view, set its style to basic and edit the labels to read:
    • Image scroll
    • Custom view scroll
    • Paged
    • Paged with peeking

Note: For item #5 above, as you change each table row’s style to “Basic”, the table row will get an additional sub-component, the label. You might need to again expand the row items to be able to see the sub-component items and to edit them.

Save the storyboard, and build and run. You should see your table view, similar to the image below. Sadly, the tableview does absolutely nothing at the moment – but we can fix that!

Scrolling and Zooming a Large Image

The first thing you’re going to learn is how to set up a scroll view that allows the user to zoom into an image and pan around.

First, you need to set up the view controller. Select ViewController.h, add an outlet for a UIScrollView called scrollView and declare that the view controller is going to be a UIScrollView delegate, like so:

#import <UIKit/UIKit.h>
 
@interface ViewController : UIViewController <UIScrollViewDelegate>
 
@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
 
@end

Then, synthesize scrollView at the top of ViewController.m, just below the @implementation line:

@synthesize scrollView = _scrollView;

Back in the storyboard, drag a view controller from the objects list onto the storyboard and set its class to ViewController.

Right-click on the first table row (either on the left sidebar or on the main storyboard view) and a connections popup will appear. Drag from the Push outlet under Storyboard Segues to the new view controller.

Alternatively, you can control click on the table row and drag to the new view controller and a popup should appear with the segue options. You can select Push from there.

Drage a scroll view from the object library to completely fill the view controller.

Wire up the scroll view to the view controller by attaching it to the scrollView outlet and setting the view controller as the scroll view’s delegate.

Now you’re going to get down and dirty with ViewController.m. First you need to declare some properties and methods in the class continuation category at the top of the file (above the @implementation line).

@interface ViewController ()
@property (nonatomic, strong) UIImageView *imageView;
 
- (void)centerScrollViewContents;
- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer;
- (void)scrollViewTwoFingerTapped:(UITapGestureRecognizer*)recognizer;
@end

Note: The class continuation category is a special category which is only allowed in the implementation file of the class and has no specified name. It allows you to specify properties and methods (and even instance variables) that are private.

That should give you a sneak peek of what you’re going to be doing in a moment. But first, synthesize the property you just added.

@synthesize imageView = _imageView;

And now it’s time to get into the most interesting part of setting up the scroll view. Replace viewDidLoad and viewWillAppear: with the following code:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // 1
    UIImage *image = [UIImage imageNamed:@"photo1.png"];
    self.imageView = [[UIImageView alloc] initWithImage:image];
    self.imageView.frame = (CGRect){.origin=CGPointMake(0.0f, 0.0f), .size=image.size};
    [self.scrollView addSubview:self.imageView];
 
    // 2
    self.scrollView.contentSize = image.size;
 
    // 3
    UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewDoubleTapped:)];
    doubleTapRecognizer.numberOfTapsRequired = 2;
    doubleTapRecognizer.numberOfTouchesRequired = 1;
    [self.scrollView addGestureRecognizer:doubleTapRecognizer];
 
    UITapGestureRecognizer *twoFingerTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTwoFingerTapped:)];
    twoFingerTapRecognizer.numberOfTapsRequired = 1;
    twoFingerTapRecognizer.numberOfTouchesRequired = 2;
    [self.scrollView addGestureRecognizer:twoFingerTapRecognizer];
}
 
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
 
    // 4
    CGRect scrollViewFrame = self.scrollView.frame;
    CGFloat scaleWidth = scrollViewFrame.size.width / self.scrollView.contentSize.width;
    CGFloat scaleHeight = scrollViewFrame.size.height / self.scrollView.contentSize.height;
    CGFloat minScale = MIN(scaleWidth, scaleHeight);
    self.scrollView.minimumZoomScale = minScale;
 
    // 5
    self.scrollView.maximumZoomScale = 1.0f;
    self.scrollView.zoomScale = minScale;
 
    // 6
    [self centerScrollViewContents];
}

This might look complicated, so let’s break it down step-by-step. You’ll see it’s really not too bad.

  1. First, you need to create an image view with the photo1.png image you added to your project and you set the image view frame (it’s size and position) so it’s the size of the image and sits at point 0,0 within the parent. Finally, the image view gets added as a subview of your scroll view.
  2. You have to tell your scroll view the size of the content contained within it, so that it knows how far it can scroll horizontally and vertically. In this case, it’s the size of the image.
  3. Here you’re setting up two gesture recognizers: one for the double-tap to zoom in, and one for the two-finger-tap to zoom out. If you’re unfamiliar with how these work, then I suggest reading this tutorial.
  4. Next, you need to work out the minimum zoom scale for the scroll view. A zoom scale of one means that the content is displayed at normal size. A zoom scale below one shows the content zoomed out, while a zoom scale of greater than one shows the content zoomed in.

    To get the minimum zoom scale, you calculate how far you’d need to zoom out so that the image fits snugly in your scroll view’s bounds based on its width. Then you do the same based upon the image’s height. The minimum of those two resulting zoom scales will be the scroll view’s minimum zoom scale. That gives you a zoom scale where you can see the entire image when fully zoomed out.

  5. You set the maximum zoom scale as 1, because zooming in more than the image’s resolution can support will cause it to look blurry. You set the initial zoom scale to be the minimum, so that the image starts fully zoomed out.
  6. We’ll come back to this in a bit. For now, just understand that this will center the image within the scroll view.

You might ask why we don’t do all of the above in viewDidLoad, and you’d be right to ask. The reason you can’t is that the view size isn’t definitely known until viewWillAppear:, and since you use the size of scrollView when calculating the minimum zoom, things might go wrong if we do it in viewDidLoad.

Now let’s look at those methods that I said you’d implement – centerScrollViewContents, scrollViewDoubleTapped: and scrollViewTwoFingerTapped:. Add the following code above viewDidLoad:

- (void)centerScrollViewContents {
    CGSize boundsSize = self.scrollView.bounds.size;
    CGRect contentsFrame = self.imageView.frame;
 
    if (contentsFrame.size.width < boundsSize.width) {
        contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
    } else {
        contentsFrame.origin.x = 0.0f;
    }
 
    if (contentsFrame.size.height < boundsSize.height) {
        contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
    } else {
        contentsFrame.origin.y = 0.0f;
    }
 
    self.imageView.frame = contentsFrame;
}

The point of this method is to get around a slight annoyance with UIScrollView, which is: if the scroll view content size is smaller than its bounds, then it sits at the top-left rather than in the center. Since you’ll be allowing the user to zoom out fully, you’d rather like the image to sit in the center of the view, wouldn’t you? :] This method accomplishes that by positioning the image view such that it is always in the center of the scroll view’s bounds.

Next up is scrollViewDoubleTapped:. Add this just above viewDidLoad again:

- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer {
    // 1
    CGPoint pointInView = [recognizer locationInView:self.imageView];
 
    // 2
    CGFloat newZoomScale = self.scrollView.zoomScale * 1.5f;
    newZoomScale = MIN(newZoomScale, self.scrollView.maximumZoomScale);
 
    // 3
    CGSize scrollViewSize = self.scrollView.bounds.size;
 
    CGFloat w = scrollViewSize.width / newZoomScale;
    CGFloat h = scrollViewSize.height / newZoomScale;
    CGFloat x = pointInView.x - (w / 2.0f);
    CGFloat y = pointInView.y - (h / 2.0f);
 
    CGRect rectToZoomTo = CGRectMake(x, y, w, h);
 
    // 4
    [self.scrollView zoomToRect:rectToZoomTo animated:YES];
}

This method is called when the tap gesture recognizer fires. Remember, you set that up to recognize double-tap events. Here’s a step-by-step guide to what scrollViewDoubleTapped: does:

  1. First, you need to work out where the tap occurred within the image view. You’ll use this to zoom in directly on that point, which is probably what you’d expect as a user.
  2. Next, you calculate a zoom scale that’s zoomed in 150%, but capped at the maximum zoom scale you specified in viewDidLoad.
  3. Then you use the location from step #1 to calculate a CGRect rectangle that you want to zoom in on.
  4. Finally, you need to tell the scroll view to zoom in, and here you animate it, as that will look pretty.

The final method to implement is scrollViewTwoFingerTapped:. Once again, add this above viewDidLoad:

- (void)scrollViewTwoFingerTapped:(UITapGestureRecognizer*)recognizer {
    // Zoom out slightly, capping at the minimum zoom scale specified by the scroll view
    CGFloat newZoomScale = self.scrollView.zoomScale / 1.5f;
    newZoomScale = MAX(newZoomScale, self.scrollView.minimumZoomScale);
    [self.scrollView setZoomScale:newZoomScale animated:YES];
}

This is similar to the way you zoomed in, except you don’t bother calculating anything about where the user tapped in the view, because it doesn’t particularly matter. Trust me, it’ll look good if you just do it like this. :]

Now, remember how you set up ViewController as a UIScrollView delegate? Well, now you’re going to implement a couple of needed methods for a UIScrollView delegate. The first is viewForZoomingInScrollView. Add the following above viewDidLoad:

- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    // Return the view that you want to zoom
    return self.imageView;
}

This is the heart and soul of the scroll view’s zooming mechanism. You’re telling it which view should be made bigger and smaller when the scroll view is pinched. So, you tell it that it’s your imageView.

The second method you’ll need to implement is scrollViewDidZoom:, which is a notification when the scroll view has been zoomed. Here you need to re-center the view – if you don’t, the scroll view won’t appear to zoom naturally, instead, it will sort of stick to the top-left. Add this above viewDidLoad:

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    // The scroll view has zoomed, so you need to re-center the contents
    [self centerScrollViewContents];
}

Now take a deep breath, give yourself a pat on the back and build and run your project. Tap on Image scroll and if everything went smoothly, you’ll end up with a lovely image that you can zoom, pan and tap. w00t!

Scrolling and Zooming a View Hierarchy

What if you want more than an image in your scroll view? What if you’ve got some complex view hierarchy which you want to be able to zoom and pan around? Well, there’s a scroll view for that! What’s more, it’s just a small step beyond what you’ve done already.

Create a new file with the iOS\Cocoa Touch\UIViewController subclass template. Name the class CustomScrollViewController and make sure that Targeted for iPad and With XIB for user interface are not checked. Click Next and save it with the rest of the project.

Open CustomScrollViewController.h and replace the contents with this:

#import <UIKit/UIKit.h>
 
@interface CustomScrollViewController : UIViewController <UIScrollViewDelegate>
 
@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
 
@end

Next, go to MainStoryboard.storyboard and just as before, add a view controller that’s wired up with a push segue from the 2nd row of the table. This time, set the view controller’s class to be the class just created, CustomScrollViewController.

Also add a scroll view and connect it to the outlet created and set the view controller as its delegate, just as before.

Then, open CustomScrollViewController.m and set up the class continuation category at the top (above the @implementation line) like this:

@interface CustomScrollViewController ()
@property (nonatomic, strong) UIView *containerView;
 
- (void)centerScrollViewContents;
@end

And add the property synthesizers below the @implementation line:

@synthesize scrollView = _scrollView;
@synthesize containerView = _containerView;

You’ll probably notice the lack of gesture recognizer callbacks. That is simply to make this part of the tutorial more straightforward. Feel free to add them in afterwards as an additional exercise.

The only other difference compared to the previous view controller is that instead of a UIImageView, we’ve got a UIView and it’s called containerView. That should be a little hint as to how this is all going to work.

Now, implement viewDidLoad and viewWillAppear: like so.

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // Set up the container view to hold your custom view hierarchy
    CGSize containerSize = CGSizeMake(640.0f, 640.0f);
    self.containerView = [[UIView alloc] initWithFrame:(CGRect){.origin=CGPointMake(0.0f, 0.0f), .size=containerSize}];
    [self.scrollView addSubview:self.containerView];
 
    // Set up your custom view hierarchy
    UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 640.0f, 80.0f)];
    redView.backgroundColor = [UIColor redColor];
    [self.containerView addSubview:redView];
 
    UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 560.0f, 640.0f, 80.0f)];
    blueView.backgroundColor = [UIColor blueColor];
    [self.containerView addSubview:blueView];
 
    UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(160.0f, 160.0f, 320.0f, 320.0f)];
    greenView.backgroundColor = [UIColor greenColor];
    [self.containerView addSubview:greenView];
 
    UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"slow.png"]];
    imageView.center = CGPointMake(320.0f, 320.0f);
    [self.containerView addSubview:imageView];
 
    // Tell the scroll view the size of the contents
    self.scrollView.contentSize = containerSize;
}
 
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
 
    // Set up the minimum & maximum zoom scales
    CGRect scrollViewFrame = self.scrollView.frame;
    CGFloat scaleWidth = scrollViewFrame.size.width / self.scrollView.contentSize.width;
    CGFloat scaleHeight = scrollViewFrame.size.height / self.scrollView.contentSize.height;
    CGFloat minScale = MIN(scaleWidth, scaleHeight);
 
    self.scrollView.minimumZoomScale = minScale;
    self.scrollView.maximumZoomScale = 1.0f;
    self.scrollView.zoomScale = 1.0f;
 
    [self centerScrollViewContents];
}

You might be feeling a sense of deja-vu here, as it’s very familiar code. In fact, viewWillAppear: is almost identical to the previous code, except for the line where you set the zoomScale. Here, we set the zoomScale to 1 instead of minScale so that we’d have the content view at normal size instead of it fitting the screen. Since we aren’t going to implement the zoom handlers, if the view fits the screen, you will not be able to test the scroll view by panning the view around.

viewDidLoad, however, sets up a view hierarchy with a single root view, which is your instance variable, containerView. Then that single view is added to the scroll view. That is the key here – just one view can be added to the scroll view if you’re going to be zooming in, because as you’ll recall, you can only return one view in the delegate callback, viewForZoomingInScrollView:.

Again, implement centerScrollViewContents and the two UIScrollView delegate methods, substituting imageView with containerView from the original versions. (You can add the code above viewDidLoad, as before.)

- (void)centerScrollViewContents {
    CGSize boundsSize = self.scrollView.bounds.size;
    CGRect contentsFrame = self.containerView.frame;
 
    if (contentsFrame.size.width < boundsSize.width) {
        contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
    } else {
        contentsFrame.origin.x = 0.0f;
    }
 
    if (contentsFrame.size.height < boundsSize.height) {
        contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
    } else {
        contentsFrame.origin.y = 0.0f;
    }
 
    self.containerView.frame = contentsFrame;
}
 
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    // Return the view that we want to zoom
    return self.containerView;
}
 
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    // The scroll view has zoomed, so we need to re-center the contents
    [self centerScrollViewContents];
}

Now build and run your project. This time, select Custom view scroll and watch in amazement as you can pan around a beautifully hand-crafted scene. (If you add in the gesture recognizers from the previous code, you’ll even be able to zoom in and out.)

Paging with UIScrollView

In the third part of this tutorial, you’ll be creating a scroll view that allows paging. This means that the scroll view locks onto a page when you stop dragging. You’ll see this in action in the Apple App Store app when you view screenshots of an app, for instance.

Create a new file with the iOS\Cocoa Touch\UIViewController subclass template. Name the class PagedScrollViewController and make sure that Targeted for iPad and With XIB for user interface are not checked. Click Next and save it with the rest of the project.

Open PagedScrollViewController.h and set it up like so:

#import <UIKit/UIKit.h>
 
@interface PagedScrollViewController : UIViewController <UIScrollViewDelegate>
 
@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
@property (nonatomic, strong) IBOutlet UIPageControl *pageControl;
 
@end

Next, go to MainStoryboard.storyboard and just like before, add a view controller that’s wired up with a push segue from the 3rd row of the table. This time, set the view controller’s class to be the class just created, PagedScrollViewController.

Also, add and wire up a scroll view to the outlet created, and set the view controller as its delegate, just like before.

This time, turn on Paging Enabled for the scroll view via the Attributes Inspector.

Also, add a Page Control element at the bottom of the view (if you made the scroll view fit the screen, you might need to adjust the height of the scroll view first) and make it fill the width. Wire it up to the pageControl outlet. Be careful not to add it as a subview of the scroll view, but rather as a sibling of the scroll view. It should look like this in Interface Builder:

Finally, set the background color of the main view to be black, so that the page control will be visible (it is white by default, and white on white really doesn’t work!).

Now open PagedScrollViewController.m and once again set up the class continuation category:

@interface PagedScrollViewController ()
@property (nonatomic, strong) NSArray *pageImages;
@property (nonatomic, strong) NSMutableArray *pageViews;
 
- (void)loadVisiblePages;
- (void)loadPage:(NSInteger)page;
- (void)purgePage:(NSInteger)page;
@end

And synthesize instance variables:

@synthesize scrollView = _scrollView;
@synthesize pageControl = _pageControl;
 
@synthesize pageImages = _pageImages;
@synthesize pageViews = _pageViews;

You’ll notice a lot of differences this time. There’s no container view, there are two arrays and there are three new methods. I’ll get to the methods in due course but for now, let me explain the arrays:

  • pageImages: This will hold all the images to display – 1 per page.
  • pageViews: This will hold instances of UIImageView to display each image on its respective page. It’s a mutable array, because you’ll be loading the pages lazily (i.e. as and when you need them) so you need to be able to insert and delete from the array.

Next, implement viewDidLoad and viewWillAppear: as follows:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // 1
    self.pageImages = [NSArray arrayWithObjects:
                       [UIImage imageNamed:@"photo1.png"],
                       [UIImage imageNamed:@"photo2.png"],
                       [UIImage imageNamed:@"photo3.png"],
                       [UIImage imageNamed:@"photo4.png"],
                       [UIImage imageNamed:@"photo5.png"],
                       nil];
 
    NSInteger pageCount = self.pageImages.count;
 
    // 2
    self.pageControl.currentPage = 0;
    self.pageControl.numberOfPages = pageCount;
 
    // 3
    self.pageViews = [[NSMutableArray alloc] init];
    for (NSInteger i = 0; i < pageCount; ++i) {
        [self.pageViews addObject:[NSNull null]];
    }
}
 
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
 
    // 4
    CGSize pagesScrollViewSize = self.scrollView.frame.size;
    self.scrollView.contentSize = CGSizeMake(pagesScrollViewSize.width * self.pageImages.count, pagesScrollViewSize.height);
 
    // 5
    [self loadVisiblePages];
}

Breaking that down, this is what happens:

  1. First, you set up the page images. You’ve added five photos to the project and so you just make an array containing all of them.
  2. Then, you tell the page control how many pages there are, and that it should start on page 0.
  3. Next, you set up the array that holds the UIImageView instances. At first, no pages have been lazily loaded and so you just fill it with the right amount of NSNull objects that are needed – one for each page. You’re using [NSNull null] because it’s a lightweight singleton object that can be added to an array to signify a placeholder. Later on, you’ll use the fact that there is an NSNull in there to know if that page is loaded or not.
  4. The scroll view, as before, needs to know its content size. Since you want a horizontal paging scroll view (it could just as easily be vertical if you want), you calculate the width to be the number of pages multiplied by the width of the scroll view. The height of the content is the same as the height of the scroll view.
  5. You’re going to need some pages shown initially, so you call loadVisiblePages, which you’ll implement shortly.

It’s time to implement the three methods defined earlier – loadVisiblePages, loadPage: and purgePage:.

Add the following above viewDidLoad:

- (void)loadPage:(NSInteger)page {
    if (page < 0 || page >= self.pageImages.count) {
        // If it's outside the range of what you have to display, then do nothing
        return;
    }
 
    // 1
    UIView *pageView = [self.pageViews objectAtIndex:page];
    if ((NSNull*)pageView == [NSNull null]) {
        // 2
        CGRect frame = self.scrollView.bounds;
        frame.origin.x = frame.size.width * page;
        frame.origin.y = 0.0f;
 
        // 3
        UIImageView *newPageView = [[UIImageView alloc] initWithImage:[self.pageImages objectAtIndex:page]];
        newPageView.contentMode = UIViewContentModeScaleAspectFit;
        newPageView.frame = frame;
        [self.scrollView addSubview:newPageView];
        // 4
        [self.pageViews replaceObjectAtIndex:page withObject:newPageView];
    }
}

Breaking that down:

  1. First, you check if you’ve already loaded the view. If you haven’t, then the object in the pageViews array will be an NSNull (remember, [NSNull null] is a special singleton which is why == works).
  2. If you made it into the if statement, then you need to create a page. So first, work out the frame for this page. It’s calculated as being the same size as the scroll view, positioned at zero y offset, and then offset by the width of a page multiplied by the page number in the x (horizontal) direction.
  3. This creates a new UIImageView, sets it up and adds it to the scroll view.
  4. Finally, you replace the NSNull in the pageViews array with the view you’ve just created, so that if this page was asked to load again you would now not go into the if statement and instead do nothing since the view for the page has already been created.

Next, add this above viewDidLoad:

- (void)purgePage:(NSInteger)page {
    if (page < 0 || page >= self.pageImages.count) {
        // If it's outside the range of what you have to display, then do nothing
        return;
    }
 
    // Remove a page from the scroll view and reset the container array
    UIView *pageView = [self.pageViews objectAtIndex:page];
    if ((NSNull*)pageView != [NSNull null]) {
        [pageView removeFromSuperview];
        [self.pageViews replaceObjectAtIndex:page withObject:[NSNull null]];
    }
}

This method purges a page that was previously created via loadPage:. It first checks that the object in the pageViews array for this page is not an NSNull. If it’s not, it removes the view from the scroll view and updates the pageViews array with an NSNull again to indicate that this page is no longer there.

Why bother lazy loading, you ask? Well, in this example, it won’t matter too much if you load all the pages at the start, since there’s only five and they won’t be large enough to eat up too much memory. But imagine you had 100 pages and each image was 5MB in size. That would take up 500MB of memory if you loaded all the pages at once! Your app would bomb out, exceeding the amount of memory available. Lazy loading means that you’ll only have a certain number of pages in memory at any given time.

The two methods we defined above are tied together via a method called loadVisiblePages. Add the following above viewDidLoad:

- (void)loadVisiblePages {
    // First, determine which page is currently visible
    CGFloat pageWidth = self.scrollView.frame.size.width;
    NSInteger page = (NSInteger)floor((self.scrollView.contentOffset.x * 2.0f + pageWidth) / (pageWidth * 2.0f));
 
    // Update the page control
    self.pageControl.currentPage = page;
 
    // Work out which pages you want to load
    NSInteger firstPage = page - 1;
    NSInteger lastPage = page + 1;
 
    // Purge anything before the first page
    for (NSInteger i=0; i<firstPage; i++) {
        [self purgePage:i];
    }
 
	// Load pages in our range
    for (NSInteger i=firstPage; i<=lastPage; i++) {
        [self loadPage:i];
    }
 
	// Purge anything after the last page
    for (NSInteger i=lastPage+1; i<self.pageImages.count; i++) {
        [self purgePage:i];
    }
}

Here you work out what page the scroll view is currently on, update the page control and then load or purge the relevant pages. The calculation of what page you’re on looks a bit scary, but it’s not too bad. You can convince yourself it’s correct by plugging some numbers in. (Note that the floor() function will round a decimal number to the next lowest integer.)

You choose to load the current page and the page on either side of it. This is so that as the user starts scrolling, they can see the next page before it becomes the central one. You could load the previous and next two pages if you wanted, but this would increase memory usage and serves no useful purpose.

The final thing to do is to implement part of the UIScrollView delegate. This time you just need to implement scrollViewDidScroll:. Add this to PagedScrollViewController.m above viewDidLoad:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // Load the pages that are now on screen
    [self loadVisiblePages];
}

All this does is ensure that as the scroll view is scrolled, the relevant pages are always loaded (and that unnecessary pages are purged).

Build and run the project, select Paged and marvel at the wonderful paged scroll view you’ve just created!

Viewing Previous/Next Pages

For the final addition to this project, I’m going to show you how you can make a scroll view that looks a lot like the screenshot viewer when browsing apps on the App Store app. You get to see parts of the previous and next pages, and it’s a great technique because the user can immediately see there’s extra content they can scroll through.

Create a new file with the iOS\Cocoa Touch\UIViewController subclass template. Name the class PeekPagedScrollViewController and maker sure that Targeted for iPad and With XIB for user interface are not checked. Click Next and save it with the rest of the project.

Open PeekPagedScrollViewController.h and set it up like so:

#import <UIKit/UIKit.h>
 
@interface PeekPagedScrollViewController : UIViewController <UIScrollViewDelegate>
 
@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
@property (nonatomic, strong) IBOutlet UIPageControl *pageControl;
 
@end

Next, open PeekPagedScrollViewController.m. Set up the class continuation category as before:

@interface PeekPagedScrollViewController ()
@property (nonatomic, strong) NSArray *pageImages;
@property (nonatomic, strong) NSMutableArray *pageViews;
 
- (void)loadVisiblePages;
- (void)loadPage:(NSInteger)page;
- (void)purgePage:(NSInteger)page;
@end

And synthesize the instance variables:

@synthesize scrollView = _scrollView;
@synthesize pageControl = _pageControl;
 
@synthesize pageImages = _pageImages;
@synthesize pageViews = _pageViews;

Then implement viewDidLoad and viewWillAppear:, which are the same as in the previous section’s paged scroll view example.

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // Set up the image you want to scroll & zoom and add it to the scroll view
    self.pageImages = [NSArray arrayWithObjects:
                       [UIImage imageNamed:@"photo1.png"],
                       [UIImage imageNamed:@"photo2.png"],
                       [UIImage imageNamed:@"photo3.png"],
                       [UIImage imageNamed:@"photo4.png"],
                       [UIImage imageNamed:@"photo5.png"],
                       nil];
 
    NSInteger pageCount = self.pageImages.count;
 
    // Set up the page control
    self.pageControl.currentPage = 0;
    self.pageControl.numberOfPages = pageCount;
 
    // Set up the array to hold the views for each page
    self.pageViews = [[NSMutableArray alloc] init];
    for (NSInteger i = 0; i < pageCount; ++i) {
        [self.pageViews addObject:[NSNull null]];
    }
}
 
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
 
    // Set up the content size of the scroll view
    CGSize pagesScrollViewSize = self.scrollView.frame.size;
    self.scrollView.contentSize = CGSizeMake(pagesScrollViewSize.width * self.pageImages.count, pagesScrollViewSize.height);
 
    // Load the initial set of pages that are on screen
    [self loadVisiblePages];
}

Then, implement loadVisiblePages, loadPage:, purgePage:, and the scrollViewDidScroll: UIScrollView delegate method, which are also identical to the previous section, except for a slight difference in loadPage, explained below.

- (void)loadVisiblePages {
    // First, determine which page is currently visible
    CGFloat pageWidth = self.scrollView.frame.size.width;
    NSInteger page = (NSInteger)floor((self.scrollView.contentOffset.x * 2.0f + pageWidth) / (pageWidth * 2.0f));
 
    // Update the page control
    self.pageControl.currentPage = page;
 
    // Work out which pages you want to load
    NSInteger firstPage = page - 1;
    NSInteger lastPage = page + 1;
 
    // Purge anything before the first page
    for (NSInteger i=0; i<firstPage; i++) {
        [self purgePage:i];
    }
    for (NSInteger i=firstPage; i<=lastPage; i++) {
        [self loadPage:i];
    }
    for (NSInteger i=lastPage+1; i<self.pageImages.count; i++) {
        [self purgePage:i];
    }
}
 
- (void)loadPage:(NSInteger)page {
    if (page < 0 || page >= self.pageImages.count) {
        // If it's outside the range of what we have to display, then do nothing
        return;
    }
 
    // Load an individual page, first checking if you've already loaded it
    UIView *pageView = [self.pageViews objectAtIndex:page];
    if ((NSNull*)pageView == [NSNull null]) {
        CGRect frame = self.scrollView.bounds;
        frame.origin.x = frame.size.width * page;
        frame.origin.y = 0.0f;
        frame = CGRectInset(frame, 10.0f, 0.0f);
 
        UIImageView *newPageView = [[UIImageView alloc] initWithImage:[self.pageImages objectAtIndex:page]];
        newPageView.contentMode = UIViewContentModeScaleAspectFit;
        newPageView.frame = frame;
        [self.scrollView addSubview:newPageView];
        [self.pageViews replaceObjectAtIndex:page withObject:newPageView];
    }
}
 
- (void)purgePage:(NSInteger)page {
    if (page < 0 || page >= self.pageImages.count) {
        // If it's outside the range of what you have to display, then do nothing
        return;
    }
 
    // Remove a page from the scroll view and reset the container array
    UIView *pageView = [self.pageViews objectAtIndex:page];
    if ((NSNull*)pageView != [NSNull null]) {
        [pageView removeFromSuperview];
        [self.pageViews replaceObjectAtIndex:page withObject:[NSNull null]];
    }
}
 
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // Load the pages that are now on screen
    [self loadVisiblePages];
}

The only difference in the above methods from those implemented in the earlier sections is the addition of the following line of code in loadPage:.

frame = CGRectInset(frame, 10.0f, 0.0f);

This line sets the frame of the image view to be slightly inset horizontally, such that the pages don’t touch. It makes it look pretty, similar to the screenshots viewer in the App Store app.

Now go to MainStoryboard.storyboard, and just as in the previous example, add a view controller that’s wired up with a push segue from the 4th row of the table. This time, set the view controller’s class to be the class just created, PeekPagedScrollViewController.

Also add and wire up a paging enabled scroll view to the outlet created, and set the view controller as its delegate, just as before. And again, add a page control element and wire it up. And don’t forget to set the main view background to black so that the paging control shows up.

Make the scroll view smaller than the screen – my suggestion is 240×312 – and center it on the screen. It should end up looking like this:

Next, turn off Clip Subviews for the scroll view. This will allow it to draw outside of its view, which is important for the peeking of pages.

Build and run, choose Paged with peeking, and there you have it! Well done!

Detecting Touches Outside a Scroll View

You may have just noticed with the peeking pages that you now can’t click outside of the scroll view region. That’s not ideal now, is it? But we can fix it!

The problem is that the scroll view only gets the touches if they occur within its bounds, and now that the bounds are smaller than the area it draws into (because Clip Subviews is off), it will miss some touches. We’re going to fix it by wrapping the scroll view in a container view whose job it is to intercept touches and hand them off to the scroll view.

Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class ScrollViewContainer, and make it a subclass of UIView. Click Next and save it along with the rest of the project.

Open ScrollViewContainer.h and replace its contents with the following:

#import <UIKit/UIKit.h>
 
@interface ScrollViewContainer : UIView
 
@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;
 
@end

You’re going to need to tell the view which scroll view it’s wrapping, that’s why you’ve added an outlet for a UIScrollView.

Open ScrollViewContainer.m and replace its contents with the following:

#import "ScrollViewContainer.h"
 
@implementation ScrollViewContainer
 
@synthesize scrollView = _scrollView;
 
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
        return _scrollView;
    }
    return view;
}
 
@end

That’s simple, right? I bet you thought there’d be lines and lines of code. Well, not today. All this does is hand control over to the scroll view for any touches that occur within the container view’s bounds.

Now you need to actually use the new container view you’ve created.

Go to MainStoryboard.storyboard and back to the view controller for this example. Select the scroll view and then click Editor\Embed In\View. This should have created a view that the scroll view now sits within.

Make this new view the width of the screen and the height of the scroll view. Then set its class to ScrollViewContainer. Also wire up the scrollView outlet of the container to the scroll view. It should then look like this:

If you set the background for the main view to black so that the page control would show up, then don’t forget to set the background for the container view to black as well. Since it sits on top of the main view, the container view will, by default, have a white background.

Build and run. Choose Paged with peeking. Notice you can now tap outside the scroll view’s bounds as you desire. w00t! How cool is that and all done with just a few lines of code!

Where to Go From Here?

Here is an example project with all of the code from this tutorial.

You’ve delved into many of the interesting things that a scroll view is capable of. If you feel confident about what you’ve done here, you might want to attempt the following extras:

  • Create a vertically paging scroll view.
  • Embed a zoomable scroll view within a paging scroll view so that each page can be zoomed and panned individually.
  • Embed a series of vertically paging scroll views within a horizontally paging scroll view to give the illusion of a 2D grid.

Now go make some awesome apps safe in the knowledge that you’ve got mad scroll view skills!

And if you run into any problems along the way or want to leave feedback about what you’ve read here, I’ll be responding to questions and comments in the forums.


This is a blog post by iOS Tutorial Team member Matt Galloway, founder of SwipeStack, a mobile development team based in London, UK.

Matt Galloway
Matt Galloway

Matt is the founder of SwipeStack, a mobile development company based in London, UK which create apps for clients and also a few of their own. One client's app was featured by Apple in the best apps of 2011, the App Rewind in the newsstand category.

BeerMap is one of their own creations, helping beer drinkers find a great pub near them! You can find him on Twitter.

User Comments

188 Comments

[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ]
  • When this is run on a ios7 device as other comments mention there appears to be an issue with centering the imageView. 'centerScrollViewContents' works out the correct values for origin.y however when they are applied to the view on IOS7 it appears that 0 is seen as under the status bar instead of the top TOP of the view. Therefore the center is not being set correctly. I have tried changing extends under bars etc and logged out the relevant frames/bounds but all seems correct. I have also changed the scrollView content inset to ensure it is 0,0,0,0 but to no avail.

    After a while I found the following which resolves the issue:

    if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) {
    self.automaticallyAdjustsScrollViewInsets = NO;
    }
    StuartMorris
  • hey only 2 images loaded in my device out of 5 mentin in code..wts d problem??
    ayush
  • hey only 2 images loaded in my device out of 5 mentin in code..wts d problem??
    ayush
  • hey..only 2 images loaded in my device out of 5 mention in d code..wts d problem??
    ayush
  • If you are looking to implement auto rotation with this you would also need to add the following:
    ```-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    // When the orientation is changed the contentSize is reset when the frame changes. Setting this back to the relevant image size
    self.mainScrollView.contentSize = self.mainImageView.image.size;
    // Reset the scales depending on the change of values
    [self setupScales];
    }```
    StuartMorris
  • How can i make Circular scrolling?


    Thank you
    ZuzooVn
  • Demo Crash in iOS7 when access scroll view and back to previous viewcontroller
    yehia
  • Demo Crash in iOS7 when access scroll view and back to previous viewcontroller
    yehia
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Kirill Muzykov

... 55 total!

Editorial Team

  • Alexis Gallagher

... 21 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Dave Harry

... 38 total!

Subject Matter Experts

  • Richard Casey

... 4 total!