35

I've been working on a simple test app to learn the ins and outs of the UIPageViewController. I have it working but I'm not convinced my execution is the best way. I hope some of you can point me in the right direction.

To get a basic understanding I used this tutorial as a starting point. http://www.appcoda.com/uipageviewcontroller-storyboard-tutorial/

The tutorial creates an app that uses one viewController for each of the pages presented by the UIPageViewController. However I need to utilize the UIPageViewController to scroll thru pages that have completely different layouts. Therefore to take the tutorial a step further I created an master-detail application that uses the UIPageViewController in the detail view to display three different view controllers. I stuck with just displaying images and labels for this test app, but the app I am currently building has three viewControllers that will contain either a tableview, imageView and textViews, or some textFields.

Here is the storyboard for my test app.

Storyboard

I use the DetailViewController as the data source for the PageViewController. In viewDidLoad of the DVC I establish the labels and images that will be used in the three content view controllers firstViewController, secondViewController, and thirdViewController in this manner.

if ([[self.detailItem description] isEqualToString:@"F14's"]) {
    //Here the page titles and images arrays are created 
    _pageTitles = @[@"Grim Reapers", @"Breakin the Barrier!", @"Top Gun"];
    _pageImages = @[@"F14_Grim.jpg", @"F14boom.jpg", @"F14_topgun.jpg"];

    //Here I call a method to instantiate the viewControllers 
    FirstController *selectedController = [self viewControllerAtIndex:0];
    SecondController *nextController = [self viewControllerAtIndex:1];
    ThirdController *lastController = [self viewControllerAtIndex:2];
    [_vc addObject:selectedController];
    [_vc addObject:nextController];
    [_vc addObject:lastController];
    _vc1 = @[selectedController];


} else if ([[self.detailItem description] isEqualToString:@"F35's"]){
    //code is above is repeated

Below is the method to instantiate the viewControllers

- (UIViewController *)viewControllerAtIndex:(NSUInteger)index
{
    if (([self.pageTitles count] == 0) || (index >= [self.pageTitles count])) {
        return nil;
    }

    // Create a new view controller and pass suitable data.
    if (index == 0) {
        FirstController *fvc = [self.storyboard instantiateViewControllerWithIdentifier:@"FirstPageController"];
        fvc.imageFile = self.pageImages[index];
        fvc.titleText = self.pageTitles[index];
        fvc.pageIndex = index;
        if ([_vc count]) {
             //Here I have to replace the viewController each time it is recreated
             [_vc replaceObjectAtIndex:0 withObject:fvc];
        }
        return fvc;
    } else if (index == 1) {
//Code is repeated for remaining viewControllers

The code in viewDidLoad is one area I feel I am doing unnecassary work. I don't believe I need to instantiate all three view controllers upon loading the DVC, but I didn't know how else to provide an array for the UIPageViewControllerDataSource protocol methods (viewControllerBeforeViewController and viewControllerAfterViewController).

Here is the viewControllerBefore.. method.

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{

    NSUInteger index = [_vc indexOfObject:viewController];

    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }

    index--;

    //notice here I call my instantiation method again essentially duplicating work I have already done!
    return [self viewControllerAtIndex:index];
}

In summary it seems that I am unnecassarily recreating the view controllers with every swipe from from one page to another. Is this just how the pageViewController works or have I way over complicated the process. Any input would be great!

SOLUTION

Matt suggested an incredibly simple solution in using identifiers. In my Storyboard i simply checked the box that uses my already implemented Storyboard identifier as the Restoration Identifier

RestorationId

Then in viewDidLoad rather than creating an array of viewControllers, simply create an array of strings that match the restoration identifiers.

if ([[self.detailItem description] isEqualToString:@"F14's"]) {
    _pageTitles = @[@"Grim Reapers", @"Breakin the Barrier!", @"Top Gun"];
    _pageImages = @[@"F14_Grim.jpg", @"F14boom.jpg", @"F14_topgun.jpg"];
    FirstController *selectedController = [self viewControllerAtIndex:0];
    [_vc addObject:@"FirstPageController"];
    [_vc addObject:@"SecondPageController"];
    [_vc addObject:@"ThirdPageController"];
    _vc1 = @[selectedController];

Finally to determine the index in the delegate methods do this rather than what I was doing before:

NSString * ident = viewController.restorationIdentifier;
NSUInteger index = [_vc indexOfObject:ident];

It now works without having to unnecessarily instantiate the view controllers.

As a last note if anyone is using exactly what I have here you can get rid of the following snippet from the viewControllerAtIndex: method.

if ([_vc count]) {
     //Here I have to replace the viewController each time it is recreated
     [_vc replaceObjectAtIndex:0 withObject:fvc];
}
Community
  • 1
  • 1
Ben
  • 967
  • 3
  • 9
  • 23
  • can you upload some example code about this? thanks! – Fabrizio Guespe Apr 11 '14 at 21:08
  • @fguespe you should have everything you need here to resolve this question... not sure what more you are looking for... notice I added a solution at the bottom of my question – Ben Apr 12 '14 at 17:00
  • This is an awesome solution. But I am having trouble getting the correct page index for use in other functions. Are you putting NSString * ident etc in the root controller or within - (NSUInteger)indexOfViewController:(embBaseViewController *)viewController of the model? – malaki1974 Jul 02 '14 at 13:23
  • @malaki1974 the line `NSString *ident` is in the root controller within both of the `viewControllerBeforeViewController:` and `viewControllerAfterViewController:` delegate methods…. I'm not sure what you are trying to implement but you say you are trying to get the page index for use in other functions. You may be able to create a `currentPageIndex` property for the controller that way you can always call something like `self.currentPageIndex` in your other functions… hope that helps – Ben Jul 04 '14 at 03:22
  • Thanks - it works as expected now. – malaki1974 Jul 05 '14 at 13:06
  • 4
    Hi @Ben, despite the code being fairly self explanatory for many, seeing a working example would help a lot. Would you be able to post your code on github? – Matt Privman Oct 05 '14 at 21:35
  • @Ben, care to put your code up? thanks – Byron Coetsee Jan 19 '15 at 15:30
  • HI! I have a similar situation, but my controller's view is a xib file. I set the restoration ID in the view but when I do controller.restorationIdenfier it returns nil... any hints on this? – Nuno Gonçalves Mar 03 '15 at 06:49
  • Why do you add the images and fonts to the array? Isn't enough one? – Darius Miliauskas Dec 01 '15 at 13:46
  • Subject to [this](http://meta.stackoverflow.com/questions/289344/how-to-treat-an-old-question-which-had-an-answer-edited-into-it) meta post : You should rollback your question to it's original form. If you like you can write an answer to the question yourself or accept an answer. NEVER include the answer in your question. – mfaani Nov 15 '16 at 16:56

4 Answers4

39

First of all, you are absolutely right that the view controllers that constitute the "pages" of the UIPageViewController can be completely different in nature. Nothing whatever says that they have to be instances of the same view controller class.

Now let's get to the actual problem, which is that you very sensibly need a way to provide the next or previous view controller given the current view controller. That is, indeed, the main issue when using a page view controller.

It would not really be terrible to hold an array of view controllers. After all, a view controller is a lightweight object (it is the view that is the heavyweight object). However, you are also right that the way you're handling this seems clumsy.

My suggestion is: if you are going to hold the view controller instances in a storyboard, then why not just keep an array of their identifiers? Now you've got an array of three strings. How simple can you get? You will also need a single instance variable that keeps track of which identifier corresponds to the view controller that having its view used as the current page (so that you can work out which one is "next" or "previous"); this could just be an integer indexing into the array.

There is then absolutely nothing wrong with instantiating a view controller each time the user "turns the page". That is what you are supposed to do when a view controller is needed. And you can readily do this by identifier.

Finally, note that if you use the scroll style of page view controller, you won't even have to do that, because the page view controller caches the view controllers and stops calling the delegate methods (or, at least, calls them less).

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Matt, your suggestion is awesome! Embarrassingly enough it took me a few hours to figure out how to pull it off. This was because I new there was no way of using the StoryBoard Identifier to determine what current view controller in use. It wasn't until I was about to give up that I stumbled across the restoration identifier. Once I discovered that little gem it was a walk in the park. Thanks for the input. I feel my new execution is much less "clumsy". – Ben Feb 08 '14 at 22:04
  • The restoration identifier idea is probably more elegant than my idea, which was just to keep track of the current index in the array of identifiers. – matt Feb 09 '14 at 05:57
  • HI! I have a similar situation, but my controller's view is a xib file. I set the restoration ID in the view but when I do controller.restorationIdenfier it returns nil... any hints on this? – Nuno Gonçalves Mar 02 '15 at 18:46
  • Nice work matt, Could you check out this post. You might be able to help him. http://stackoverflow.com/questions/29745656/uipageviewcontroller-instantiating-viewcontrollers-with-storyboard-id-objectiv/29745910#29745910 – Timo Cengiz Apr 20 '15 at 14:16
  • I have one question : do `UIPageViewControllerDataSource` methods - `..After` & `..Before` pop the current ViewController being displayed before pushing the next one in line..? Or do we have to take care of that?(Given that a NavigationController is being used embedded at the UIPageViewController) – Dravidian Jul 15 '17 at 16:51
  • @matt do you have a swift example of using different controller in uiPageviewController, while pageviewcontroller is in another controller – Iraniya Naynesh Mar 22 '18 at 10:38
8

Came across this question as I was looking for a solution to this same problem - thanks to Matt for the guidance that he provided and to Ben for the solution description.

I built a sample project to understand this myself and since I noticed some comments asking for sample code I've uploaded this to GitHub. My solution mimics Matt's suggested approach & Ben's stated solution by:

  • Setting the restoration IDs in the Storyboard for each of the view controllers that are part of the PageViewController.
  • Using an NSArray of NSString objects which contain the above mentioned RestorationIDs.
  • Instantiating the appropriate view controller based on this configuration.

Additionally, the implementation problem I was trying to solve required the ability to navigate backwards/forwards from the child view controllers, so this sample project also supports that functionality by asking the root view controller to go to the previous or next page (this could also be applied to go to a specific page).

Sample code on GitHub

Side note: I was admittedly hoping that similar to a UITabBarController that I could simply wire everything up from within the Storyboard and specify the order of the view controllers but alas it doesn't seem like we're there yet (as of Xcode 6 / iOS 8.1). The code required for this solution however is pretty minimal and straightforward.

Isuru
  • 30,617
  • 60
  • 187
  • 303
Derek Lee
  • 3,452
  • 3
  • 30
  • 39
1

Basically, I managed to get a slightly different way which is based on the template provided by XCode 6.4 (Page-Based Application) methods and the insights from other authors (including this, @Ben from other answers):

- (void)viewDidLoad {
    [super viewDidLoad];

    self.viewControllersArray = @[@"FirstViewController", @"SecondViewController"];
...
}

...

- (UIViewController *)viewControllerAtIndex:(NSUInteger)index {

    UIViewController *childViewController = [self.storyboard instantiateViewControllerWithIdentifier:[self.viewControllersArray objectAtIndex:index]];
    //childViewController.index = index;
    return childViewController;

}

- (NSUInteger)indexOfViewController:(UIViewController *)viewController {
    NSString *restorationId = viewController.restorationIdentifier;
    return [self.viewControllersArray indexOfObject:restorationId];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexOfViewController:(UIViewController *)viewController];
    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }

    index--;
    return [self viewControllerAtIndex:index];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexOfViewController:(UIViewController *)viewController];
    if (index == NSNotFound) {
        return nil;
    }

    index++;
    if (index == [self.viewControllersArray count]) {
        return nil;
    }
    return [self viewControllerAtIndex:index];
}
Darius Miliauskas
  • 3,391
  • 4
  • 35
  • 53
0
@interface ViewController ()
{
    NSMutableArray * StoryboardIds;
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    StoryboardIds = [[NSMutableArray alloc]init];
    [StoryboardIds addObject:@"vc1"];
    [StoryboardIds addObject:@"vc2"];

    UIViewController *selectedController = [self viewControllerAtIndex:0];

    self.ProfilePageViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"PageControl"];
    self.ProfilePageViewController.dataSource = self;

    _viewcontrollers = [NSMutableArray new];

    [_viewcontrollers addObject:selectedController];

    [self.ProfilePageViewController setViewControllers:_viewcontrollers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];

    // Change the size of page view controller
    self.ProfilePageViewController.view.frame = CGRectMake(0, 0, self.bodypage.frame.size.width, self.bodypage.frame.size.height);

    [self addChildViewController:self.ProfilePageViewController];
    [self.ProfilePageViewController.view setFrame:self.bodypage.bounds];
    [self.bodypage addSubview:self.ProfilePageViewController.view];
    [self.ProfilePageViewController didMoveToParentViewController:self];

}
- (UIViewController *)viewControllerAtIndex:(NSUInteger)index
{
    if (([StoryboardIds count] == 0) || (index >= [StoryboardIds count])) {
        return nil;
    }

    if (index == 0) {
        vc1 *fvc = [self.storyboard instantiateViewControllerWithIdentifier:@"vc1"];
        fvc.Pageindex = index;
        if ([_viewcontrollers count]) {
            [_viewcontrollers replaceObjectAtIndex:0 withObject:fvc];
        }
        return fvc;
    }
    else
    {
        vc2 *fvc = [self.storyboard instantiateViewControllerWithIdentifier:@"vc2"];
        fvc.Pageindex = index;
        if ([_viewcontrollers count]) {
            [_viewcontrollers replaceObjectAtIndex:0 withObject:fvc];
        }
        return fvc;
    }

}

-(NSUInteger)indexofViewController
{
    UIViewController *currentView = [self.ProfilePageViewController.viewControllers objectAtIndex:0];

    if ([currentView isKindOfClass:[vc2 class]]) {
        return 1;
    }
    else{
        return 0;
    }

}


- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexofViewController];

    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }

    index--;
    return [self viewControllerAtIndex:index];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexofViewController];

    if (index == NSNotFound) {
        return nil;
    }

    index++;

    if (index == [StoryboardIds count]) {
        return nil;
    }
    return [self viewControllerAtIndex:index];
}
Raja Bhuma
  • 1
  • 1
  • 2