Advertisement

Using ScrollStyle with UIPageViewController

by

One of the changes introduced with iOS 6 is the ability to use the UIPageViewController to use a scrolling animation instead of the standard page turning style. Read on to learn how we can use this to easily create a paginated scroll view effect!

To explore these changes we will be creating an "image rater" application where users can swipe through different pictures and give a rating to each one. Although the premise is basic, it will give you a good chance to start using the UIPageControl with the new style.


Step 1: Setting Up

Before we begin, we’ll need some pictures. Jump into Wikipedia and download 5 different pictures that are licensed under a creative commons. Create a new Xcode project with an empty template. Name it "ImageRater" or something equivalent. Also make sure that the targeted device is set to iPad, storybording is off, and ARC is on.

This project is going to be designed to be used in landscape orientation only. Before we start, open up the project settings and select only the landscape orientations.


Step 2: Create The Image Model and View Controller

Next, we need to create the model object and view controller responsible for displaying the image. Create a new class called "ImageModel" inheriting from NSObject. This will be a simple model class that contains a string representing the image file name and an integer that represents the image "rating":

ImageModel.h

@interface ImageModel : NSObject

- (id)initWithImageName:(NSString *)imageName;

@property (nonatomic, strong) NSString *imageName;

@property (nonatomic) NSInteger rating;

@end

We have also created a custom init method that will make creating these with an image name easier. In the .m file implement, the init method like so:

ImageModel.m

@implementation ImageModel

- (id)initWithImageName:(NSString *)imageName
{
    self = [super init];
    if (self)
    {
        _imageName = imageName;
        _rating = 0;
    }
    
    return self;
}

The view controller that will display the image will also be very simple. Create a new class called ImageViewController (inheriting UIViewController) and give it the following properties (as well as importing ImageModel.h):

ImageViewController.h

#import "ImageModel.h"

@interface ImageViewController : UIViewController

@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UILabel *label;

@property (nonatomic, strong) ImageModel *model;

@end

And in the .m file, add the following code:

ImageViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self.view setBackgroundColor:[UIColor whiteColor]];
    
    CGRect insetFrame = CGRectMake(20, 80, self.view.frame.size.width - 40, self.view.frame.size.height - 100);
    _imageView = [[UIImageView alloc] initWithFrame:insetFrame];
    _imageView.backgroundColor = [UIColor clearColor];
    [_imageView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
    [_imageView setImage:[UIImage imageNamed:_model.imageName]];
    [[self view] addSubview:_imageView];
}

When the view loads, we will set the views background color and set up the imageView.


Step 3: Set Up the PageViewController

Create a new class called "RootViewController" (inheriting from UIViewController). This will be where the action happens. It will contain the PageViewController, the label that shows the name and ranking of the image, as well as a stepper that allows the user to set the rating for the picture.

Once you’ve created the class, open the app delegates header file, import the RootViewController class, and set it as a property:

AppDelegate.h

#import "RootViewController.h"

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@property (strong, nonatomic) RootViewController *viewController;

@end

And in the implementation file set it as the the root view controller:

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
    // Override point for customization after application launch.
    self.viewController = [[RootViewController alloc] init];
    
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}

Open the .h file of the RootViewController and give it the following properties and make it conform to the UIPageViewControllerDataSource and Delegate methods:

RootViewController.h

@interface RootViewController : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>

@property (nonatomic, strong) UIPageViewController *pageViewController;
@property (nonatomic, strong) NSMutableArray *modelArray;
@property (nonatomic) NSInteger vcIndex;
@property (nonatomic, strong) UIStepper *rateStepper;
@property (nonatomic, strong) UILabel *imageLabel;

- (void)stepperValueChanged:(id)sender;

@end

The pageViewController will be responsible for displaying the ImageViewControllers and the model array will be responsible for storing the imageModel objects. The vcIndex will be responsible for keeping track of the current page index. RateStepper will allow the user to rate an image up or down. The imageLabel will display the name and rating of the current image.

The stepperValueChanged method will update the relevant image model setting its rating up or down.

In the .m file of the RootViewController import the ImageViewController and set up the view controllers for the page view controller:

RootViewController.m

#import "ImageViewController.h"

@implementation RootViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [[self view] setBackgroundColor:[UIColor blackColor]];
    
    _modelArray = [NSMutableArray arrayWithObjects:[[ImageModel alloc] initWithImageName:@"cat.jpg"],
                   [[ImageModel alloc] initWithImageName:@"DawnFlight.jpeg"],
                   [[ImageModel alloc] initWithImageName:@"James.jpg"],
                   [[ImageModel alloc] initWithImageName:@"MOS_KIM.jpg"],
                   [[ImageModel alloc] initWithImageName:@"Pterophorus.jpg"],
                   nil];
    
    
    _pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
                                                          navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                                                                        options:[NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:50.0f] forKey:UIPageViewControllerOptionInterPageSpacingKey]];
    
    _pageViewController.delegate = self;
    _pageViewController.dataSource = self;

There are a few things going on here. First, we set our background color to black. Second, we alloc init our pageViewController with the scollStyle transition, the horizontal navigation orientation, and a dictionary for our options. The dictionary contains an NSNumber that states there should be a spacing of 50 points between each viewController. We then proceed to set ourself as the delegate and datasource for the pageViewController.

Next, we need to set up an initial viewController to start off with:

RootViewController.m

 _pageViewController.delegate = self;
    _pageViewController.dataSource = self;
    
    ImageViewController *imageViewController = [[ImageViewController alloc] init];
    imageViewController.model = [_modelArray objectAtIndex:0];
    NSArray *viewControllers = [NSArray arrayWithObject:imageViewController];

    [self.pageViewController setViewControllers:viewControllers
                                  direction:UIPageViewControllerNavigationDirectionForward
                                   animated:NO
                                 completion:nil];
    
    [self addChildViewController:_pageViewController];
    [self.view addSubview:_pageViewController.view];
    
    [_pageViewController didMoveToParentViewController:self];
    
    CGRect pageViewRect = self.view.bounds;
    pageViewRect = CGRectInset(pageViewRect, 40.0, 80.0f);
    self.pageViewController.view.frame = pageViewRect;
    
    self.view.gestureRecognizers = _pageViewController.gestureRecognizers;

Here we create a new content view controller, add it to an array, and then set that array as the viewControllers property for our pageController. Next, we add the pageViewController as our childViewController and then the pageViewControllersView to our own. We then give it a bit of an inset so that it doesn't take up the entire view and set our gesture recognizers to that of the pageViewControllers so everything is in sync.

Finally, to finish the view we need to add the stepper and label that describes the picture:

RootViewController.m

    self.view.gestureRecognizers = _pageViewController.gestureRecognizers;
    
    _rateStepper = [[UIStepper alloc] initWithFrame:CGRectMake(40, 680, 40, 30)];
    [_rateStepper addTarget:self action:@selector(stepperValueChanged:) forControlEvents:UIControlEventValueChanged];
    [_rateStepper setMinimumValue:0];
    [_rateStepper setMaximumValue:10];
    [_rateStepper setIncrementImage:[UIImage imageNamed:@"arrowup"] forState:UIControlStateNormal];
    [_rateStepper setDecrementImage:[UIImage imageNamed:@"arrowdown"] forState:UIControlStateNormal];
    [[self view] addSubview:_rateStepper];
    
    _imageLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 40)];
    _imageLabel.backgroundColor = [UIColor clearColor];
    _imageLabel.textColor = [UIColor whiteColor];
    [_imageLabel setFont:[UIFont boldSystemFontOfSize:20]];
    [_imageLabel setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
    [_imageLabel setTextAlignment:NSTextAlignmentCenter];
    ImageModel *model = [_modelArray objectAtIndex:0];
    _imageLabel.text = [NSString stringWithFormat:@"%@ - Rating: %d", model.imageName, model.rating];
    [[self view] addSubview:_imageLabel];
}

Most of this should be fairly straightforward. The main thing to pay attention to here is where we set the "increment" and "decrement" images for the stepper control. You will need to find your own images for this, but it should be easy to make or find some free icons on google. If you can’t do this, you can simply skip customizing the stepper.

If we build and run our app now, it should look something like this:

Incomplete App

This is a good start, but the images don’t scroll yet and the stepper crashes the app. We are also missing the page indicators. Let's fill in those blanks:


Step 4: Completing the PageViewController

We need to implement the datasource methods that tell the pageViewController which view controllers should be loaded before and after the current one:

RootViewController.m

#pragma mark -
#pragma mark - UIPageViewControllerDelegate Method

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    ImageViewController *contentVc = (ImageViewController *)viewController;
    
    NSUInteger currentIndex = [_modelArray indexOfObject:[contentVc model]];
    _vcIndex = currentIndex;
    [_rateStepper setValue:[[contentVc model] rating]];
    ImageModel *model = [_modelArray objectAtIndex:_vcIndex];
    [_imageLabel setText:[NSString stringWithFormat:@"%@ - Rating: %d", model.imageName, model.rating]];
    
    if (currentIndex == 0)
    {
        return nil;
    }
    
    ImageViewController *imageViewController = [[ImageViewController alloc] init];
    imageViewController.model = [_modelArray objectAtIndex:currentIndex - 1];
    return imageViewController;
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    ImageViewController *contentVc = (ImageViewController *)viewController;
    
    NSUInteger currentIndex = [_modelArray indexOfObject:[contentVc model]];
    _vcIndex = currentIndex;
    [_rateStepper setValue:[[contentVc model] rating]];
    ImageModel *model = [_modelArray objectAtIndex:_vcIndex];
    [_imageLabel setText:[NSString stringWithFormat:@"%@ - Rating: %d", model.imageName, model.rating]];
    
    if (currentIndex == _modelArray.count - 1)
    {
        return nil;
    }
    
    ImageViewController *imageViewController = [[ImageViewController alloc] init];
    imageViewController.model = [_modelArray objectAtIndex:currentIndex + 1];
    return imageViewController;
}

Both of these methods are quite similar. First, we get the relevant viewController before or after the current one (and cast it to an ImageViewController). We then look to find where the index of that viewController's model is in relation to our model array. Once found, we set our current vcIndex value to the currentIndex. We also make sure to update the stepper's current value and update the label string to display the current image name and rating value.

If the index would be out of bounds (determined by equalling 0 or the count of the model array - 1), then we do not return a new view controller. If there is a model to be loaded, we create a new ImageViewController, set it’s model to the relevant model in the array, and return the viewController.

If you build and run now, the pages will scroll, however we are still missing the page indicator view. We only need to implement the following dataSource methods as below:

RootViewController.m

#pragma mark -
#pragma mark - UIPageViewControllerDataSource Method

- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
{
    return _modelArray.count;
}

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
    return 0;
}

It’s that easy. For the count method, we simply return the count of the model array and for the current Index we simply return what it needs to start at, which is 0.


Step 5: Rating the Images

All the hard work has been done! To rank the images, simply implement our "stepperValueChanged" method and add the below code:

RootViewController.m

#pragma mark -
#pragma mark - Private Methods
- (void)stepperValueChanged:(id)sender
{
    ImageModel *model = [_modelArray objectAtIndex:_vcIndex];
    [model setRating:[_rateStepper value]];
    [_imageLabel setText:[NSString stringWithFormat:@"%@ - Rating: %d", model.imageName, model.rating]];
}

We simply get the model at the current index, update its rating based on the steppers rating, and then update the label text.

Build and run your application now and it should look something like this:

Finished App

Wrap Up

We’ve covered how to set up a very simple PageViewController using the new scroll transition style without the use of a xib or storyboard. This should give you a solid understanding of how the PageViewController works and allow you to modify it to work in your own applications!

Advertisement