Create a Sliding Window effect in iOS

As part of my learning iOS I have really started to dig into new aspects of the platform which are challenging me to better understand what iOS and Objective-C offers.  While it has been tough at times due to my familiarity with the Microsoft stack I feel that I am making progress.  One of the effects that is used heavily in Windows Phone applications is the idea of the user being able to slide (using the swipe gesture) from screen to screen; I wanted to duplicate that in iOS.

Approach 1: The ScrollView

Talking with some fellow iOS developers they recommended using a ScrollView and adding SubViews to that ScrollView to represent each of the screens.  I decided to use Storyboards as they appear to be the way forward recommended by Apple for developing UIs, and I really like the tool, it reminds me of what I wish Sketchflow had been.  However, I am finding that in many cases I am having to translate blog entries with use XIB files to Storyboard since most of that content was authored before Storyboards existed.

I found a great tutorial explaining this approach: http://www.iosdevnotes.com/2011/03/uiscrollview-paging/.  Just a word or warning if you intend to use this approach, watch out for ‘Auto Layout’.  It appears an issue exists, I can’t say for sure if its a bug or not, with storyboards.  This checkbox is checked by default when using Storyboards:

NewImage

When this box is checked the ScrollView will not come back with a size, even though it is set to fill the screen.  This is a problem since the code in the tutorial relies up on the size being there for mathematical calculations.  Deselect this checkbox and the code will work perfectly.

It might be my unfamiliarity with Objective-C/iOS but it appeared with this approach there was no way to load a view controller into the subview.  I didn’t want to A) support all of my screens from one view controller and B) I didn’t want to mix XIB and Storyboards.  Thus I began looking for an alternative approach.

Approach 2: The UIPageViewController

As it so happens, Apple has a template for this: Page Based Application

NewImage

At the center of this template is the Page View Controller (UIPageViewController).  This appears to be the Apple recommended way to create the effect we are going for.  Its actually quite simple.  I was able to deduce most of the process by analyzing the default code that comes with the template.

Given the application I am working on was already in progress, naturally I didn’t start with the Page Based Application as my starting template, so I had to add this functionality in.  Let’s walk through how I did this:

1) The very first thing I did was remove my existing “landing screen” and replace it with a Page View Controller.  I then made sure that my original MainViewController was changed to MainPageViewController and inherited from UIPageViewController.  This allowed me to select the controller from the drop down and directly associate the ViewController with the View (the Apple template does not do this, not sure why).

2) With this in place I added three View Controller views to the storyboard.  See, the Page View Controller acts as a container, these pages represent the actual content that will be shown within the container.  If you look at the default Apple template this is was what DataViewController represents, however, they are simply giving this controller different data depending on the month.  Since my application contains three discrete screens for view, with different functions, I need to create each screen individually.  Very important, we need to give each of these views an ID that we can refer to them with.  Specify it as the StoryboardId:

NewImage

You should also add some minor UI to each of your screens, this will help visualize if the effect is working or not.  Also, be sure that the Transition Style is set to scroll.  This will give you that nice transition so that it looks to the user like they are scroll:

NewImage

3) Before we get to the wire up, we need to create a controller of sorts to take care of the Data and Interaction portions for the PageView controller.  You can use your existing ViewController for this if you want, but I find that creating a separate class lends itself to better readability and keeps things from becoming congested in the MainPageViewController.  For this reason I created the PageController.  Here is the interface definition from the .h file

@interface PageController : NSObject <UIPageViewControllerDataSource, UIPageViewControllerDelegate>

You can see we are enforcing two protocols, which will dictate the methods available by this class.  However, I also added a couple of my own for ease of use

– (id) initWithViewController:(UIViewController *) controller;

– (OneListScreenViewControllerBase *) getStartingViewController;

These methods will be used by the code later on when we get to the wire up portion.  Lets walk through the methods in this class:

– (id) initWithViewController:(UIViewController *) controller {

    self = [super init];

    

    if (self) {

        // initialize the listing of view controller names

        pageViewControllerNames = [[NSMutableArray alloc] initWithCapacity:3];  // only three pages at present

        [pageViewControllerNames insertObject:@”ListActivity”atIndex:0];

        [pageViewControllerNames insertObject:@”MainListing”atIndex:1];

        [pageViewControllerNames insertObject:@”ListDetail”atIndex:2];

        

        // store a reference to the root view controller

        rootViewController = controller;

    }

    

    returnself;

}

This method is responsible for initializing the PageController (its the constructor for all intents and purposes).  Notice that we create an NSMutableArray with three strings.  These strings match the names you used for StoryboardId in Step 2.  They also represent the order, left to right, of the screens.  Finally, I am creating a reference back to whichever UIViewController is to be associated with this Page Controller.  Since all ViewController inherit from UIViewController it is the most abstract type I can use for this, this allows this to work with anything.  We will need this reference later on.

Note: Below there is a reference to a type ScreenViewControllerBase, this is a custom type I created which inherits from UIViewController and is the base class for all of my “pages” or the views that will be displayed in the container.  This was done so I could write the methods below to more general and not have to hard code which types to return at what indexes.  Here is the interface definition (.h) for this type.

#import

 

@interface ScreenViewControllerBase : UIViewController

 

@property (strong, retain) NSString *storyboardId;

@end

Now let’s discuss the methods in PageController

– (ScreenViewControllerBase *) getStartingViewController {

    return (ScreenViewControllerBase *)[selfviewControllerAtIndex:1

                                                               storyboard:rootViewController.storyboard];

}

Here we are indicating which view should be displayed when the user see’s first.  In this case I am starting at index 1, which is in the middle.  You can see the reference to rootViewController to get the storyboard.  This is the reason we used a custom override for init.

 

– (ScreenViewControllerBase *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard {

    return [storyboard instantiateViewControllerWithIdentifier:[pageViewControllerNames objectAtIndex:index]];

}

 

The rest of these methods are implemented as per the UIPageViewControllerDataSource and UIPageViewControllerDelegate protocols.

– (ScreenViewControllerBase *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard {

    return [storyboard instantiateViewControllerWithIdentifier:[pageViewControllerNames objectAtIndex:index]];

}

This method simply returns the ScreenViewControllerBase that is found at the given index.  It uses a reference to the storyboard to instantiate the view controller based on the StoryboardId.  This is why providing the StoryboardId is important, it allows us to leverage the View -> ViewController associations stored in the storyboard to return the appropriate view controller for a view. 

 

– (NSUInteger)indexOfViewController:(UIViewController *)viewController {

    ScreenViewControllerBase* oneListScreen = (ScreenViewControllerBase *) viewController;

    return [pageViewControllerNames indexOfObject:oneListScreen.storyboardId];

}

You may have noticed that ScreenViewControllerBase also exposed a property named storyboardId.  This was done to handle this method.  Here we are returning the integer position of the view controller (and the view) within our screen set.  This is used by paging to realize when we have reach the end of our listing.

 

– (ScreenViewControllerBase *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController

{

    NSUInteger index = [self indexOfViewController😦ScreenViewControllerBase *)viewController];

    if ((index == 0) || (index == NSNotFound)) {

        return nil;

    }

    

    index–;

    return [self viewControllerAtIndex:index storyboard:viewController.storyboard];

}

 

– (ScreenViewControllerBase *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController

{

    NSUInteger index = [self indexOfViewController😦ScreenViewControllerBase *)viewController];

    if (index == NSNotFound) {

        return nil;

    }

    

    index++;

    if (index == [pageViewControllerNames count]) {

        return nil;

    }

    return [self viewControllerAtIndex:index storyboard:viewController.storyboard];

}

These two methods work in conjunction with each other depending on the direction the user swipes.  One decrements the index and one increments it.  Both return nil to indicate the end of the road.  It would be possible, I presume, to simply add reset logic in these methods such that you can do a wraparound.

4) The next step is a bit boring.  We need to define a view controller (that inherits from ScreenViewControllerBase) for each view in our content.  In my case I added three:

#import “ListActivityViewController.h”

 

@implementation ListActivityViewController

 

– (void)viewDidLoad

{

    [super viewDidLoad];

    

    self.storyboardId = @”ListActivity”;

}

 

– (void)didReceiveMemoryWarning

{

    [super didReceiveMemoryWarning];

    // Dispose of any resources that can be recreated.

}

 

@end

 

#import “MainListViewController.h”

 

@implementation MainListViewController

 

– (void)viewDidLoad

{

    [superviewDidLoad];

    

    self.storyboardId = @”MainListing”;

}

 

– (void)didReceiveMemoryWarning

{

    [super didReceiveMemoryWarning];

}

 

@end

 

 

#import “ListDetailViewController.h”

 

@implementation ListDetailViewController

 

– (void)viewDidLoad

{

    [super viewDidLoad];

    

    self.storyboardId = @”ListDetail”;

}

 

– (void)didReceiveMemoryWarning

{

    [super didReceiveMemoryWarning];

}

 

@end

 

You notice that as of right now there isn’t much going on in any of these classes, but importantly look at the assignment of storyboardId and recall the purpose of this property from Step 3.  This is a textual identifier to aid in view controller look up.

After you have created these classes (remember they should ALL inherit from ScreenViewControllerBase) assign them to each of the normal View Controller visualizations on your storyboard

NewImage

We are now done with our setup, time to do the actual wire up.

5) Most of this is going to happen in our MainPageViewController, that is the controller which is acting as a container for the other views.  So lets look at the viewDidLoad method

– (void)viewDidLoad {

    [superviewDidLoad];

    

    // setup the paging system

    self.pageController = [[PageController alloc] initWithViewController:self];

    self.delegate = self.pageController;

    self.dataSource = self.pageController;

    

    // get the first view

    NSArray *viewControllers = @[[self.pageControllergetStartingViewController]];

    [self setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForwardanimated:NOcompletion:NULL];

    

    [self didMoveToParentViewController:self];

    self.view.gestureRecognizers = self.gestureRecognizers;

}

It really is nice when we properly inherit, if you looked at the default template from Apple and then at this, the simplification makes it night and day, I am simply not sure why the example doesn’t use inheritance like it should be used in this case.  In any event, only three sections here.  Since the view controller that we are in is a UIPageViewController we need only indicate the delegate and datasource for handling the paging operations.  We used our custom “constructor” initWithViewController passing an instance of ourselves to the instance of PageController.

Regrettably I have not found out how to do section two from the Storyboard yet, I am certain there must be a way, but for now we call our custom method to return the first view controller we want the user to see. Something that confuses me here is that it wants an array of view controllers, yet we only pass a single one.  This is consistent with the Apple example.  Evidently, we are leaving it to our datasource logic to dictate the next controller.  This is in line with how many other platforms do similar data sourcing operations.

I will be honest, I am not entirely sure what the last two lines are doing.  But they seem to be required and are copied directly from Apple’s example.

That is it.  Everything should be working now.  I have listed some common problem points below if you have trouble:

I get no UI when I start

Check that you are specified the “Initial View Controller” on one of your view controllers

My transition curls instead of scrolling, like a book

You need to set the transition type to Scroll.  See the very end of point 2.

I see my initial view but I can’t scroll

Check the logic in your page controller datasource, make sure it is incrementing properly.  Also, you HAVE to make sure the Storyboard Ids are set properly otherwise you will not be able to find the view controllers from the storyboard.  This will more than likely cause your application to crash instead of simply not working

Leave a comment if there are any additional problems

Advertisement

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 )

Facebook photo

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

Connecting to %s