Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add a new view controller in UIPageViewController after UIButton was tapped?

I was wondering how can I implement buttons which will affect the UIPageViewController's model. For example I would like my UIPageViewController model array to load with a single object in it and when a button is tapped a new view controller will be added and will flip automatically the page or something like that (like in the Notes app). Same for deleting the current object that the user is seeing.

So far I tried by implementing some IBActions to my root view controller but no luck so far.

Here is how I have implement my ViewController class:

@implementation ViewController

@synthesize modelArray = _modelArray;
@synthesize pageVC = _pageVC;

#pragma mark - UIPageViewControllerDataSource Methods

// Returns the view controller before the given view controller. (required)
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
      viewControllerBeforeViewController:(UIViewController *)viewController
{
    NSUInteger currentIndex = [self.modelArray indexOfObject:[(ContentVC *)viewController     labelContents]];
    if(currentIndex==0)
        return nil;

    ContentVC *cVC = [[ContentVC alloc] init];
    cVC.labelContents = [self.modelArray objectAtIndex:currentIndex-1];
    return cVC;
}

// Returns the view controller after the given view controller. (required)
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
   viewControllerAfterViewController:(UIViewController *)viewController
{
    NSUInteger currentIndex = [self.modelArray indexOfObject:[(ContentVC *)viewController labelContents]];
    if(currentIndex==self.modelArray.count-1)
        return nil;

    ContentVC *cVC = [[ContentVC alloc] init];
    cVC.labelContents = [self.modelArray objectAtIndex:currentIndex+1];
    return cVC;
}

#pragma mark - UIPageViewControllerDelegate Methods
// Returns the spine location for the given orientation.
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
               spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
    if(UIInterfaceOrientationIsPortrait(orientation)){
        // Set the array with only 1 view controller
        UIViewController *currentVC = [self.pageVC.viewControllers objectAtIndex:0];
        NSArray *viewControllers = [NSArray arrayWithObject:currentVC];
        [self.pageVC setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];

        // Set the doubleSided property to NO
        self.pageVC.doubleSided = NO;
        // Return the spine location
        return UIPageViewControllerSpineLocationMin;
    } else { // if landscape
        NSArray *viewControllers = nil;
        ContentVC *currentVC = [self.pageVC.viewControllers objectAtIndex:0];
        NSUInteger currentIndex = [self.modelArray indexOfObject:[(ContentVC *)viewControllers labelContents]];
        if(currentIndex==0 || currentIndex %2 == 0){
            UIViewController *nextViewController = [self pageViewController:self.pageVC viewControllerAfterViewController:currentVC];
            viewControllers = [NSArray arrayWithObjects:currentVC, nextViewController, nil];
        } else {
            UIViewController *previousVC = [self pageViewController:self.pageVC viewControllerBeforeViewController:currentVC];
            viewControllers = [NSArray arrayWithObjects:previousVC, currentVC, nil];
        }
        // Set the view controllers as property of the UIPageViewController
        [self.pageVC setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
        return UIPageViewControllerSpineLocationMid;
    }

}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Instantiate the model array
    self.modelArray = [[NSMutableArray alloc] init];
    [self.modelArray addObject:@"Page One"];
    NSLog(@"%@", self.modelArray);

    // Instantiate the UIPageViewController
    self.pageVC = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];

    // Assign the delegate and datasource as self
    self.pageVC.delegate = self;
    self.pageVC.dataSource = self;

    // Set the initial view controllers
    ContentVC *cVC = [[ContentVC alloc] initWithNibName:@"ContentVC" bundle:nil];
    cVC.labelContents = [self.modelArray objectAtIndex:0];
    NSArray *viewControllers = [NSArray arrayWithObject:cVC];
    [self.pageVC setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];

    // Add the pageViewController as the childViewController
    [self addChildViewController:self.pageVC];

    // Add the view of pageViewController  to the currentView
    [self.view addSubview:self.pageVC.view];

    // Call didMoveToParentViewController: of the childViewController, the UIPageViewController instance in our case.
    [self.pageVC didMoveToParentViewController:self];

    // Assign the gestureRecognizers property of the pageViewController to the view's gestureRecognizers property
    self.view.gestureRecognizers = self.pageVC.gestureRecognizers;

}

Thank you very much!

like image 951
user1644365 Avatar asked Dec 21 '22 14:12

user1644365


1 Answers

The UIPageViewController uses a datasource to obtain the next or previous view controller. So the solution is to add your new view controller to the datasource once the button is touched.

Say your datasource is implemented as an NSArray (see the XCode's Page-based Application template for an example of this) you just add your new vc to that array and set the new vc by calling [UIPageViewController setViewControllers:direction:animated:completion:].

Since you didn't provide any details on how you implemented your hierarchy I can't be more specific.

Edit:

Now that I have some code, here's what I'd do:

First I wouldn't save the labelContents but rather the view controller itself in the modelArray. Besides other things, your current design creates a new ContentVC every time you change pages. That's a lot of unnecessary overhead. Here's how I would implement that:

(btw, you should think of a more descriptive name than labelContents. Right now it might be fine if there's just one label, but what if you add more labels in the future?)

- (ContentVC *)contentViewControllerWithLabelContents:(NSString *)labelContents
{
    ContentVC *vc = [[ContentVC alloc] initWithNibName:@"ContentVC" bundle:nil];
    vc.labelContents = labelContents;
    return vc;
}

- (void)viewDidLoad
{
    NSMutableArray *vcs = [NSMutableArray array];
    [vcs addObject:[self contentViewControllerWithLabelContents:@"Page One"]];
    [vcs addObject:[self contentViewControllerWithLabelContents:@"Page Two"]];
    //...
    self.modelArray = vcs;
}


#pragma mark - UIPageViewControllerDataSource Methods

// Returns the view controller before the given view controller. (required)
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
      viewControllerBeforeViewController:(UIViewController *)viewController
{
    NSUInteger currentIndex = [self.modelArray indexOfObject:viewController];
    if(currentIndex == 0)
        return nil;

    ContentVC *cVC = [self.modelArray objectAtIndex:currentIndex - 1];
    return cVC;
}

// Returns the view controller after the given view controller. (required)
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
   viewControllerAfterViewController:(UIViewController *)viewController
{
    NSUInteger currentIndex = [self.modelArray indexOfObject:viewController];
    if(currentIndex == self.modelArray.count - 1)
        return nil;

    ContentVC *cVC = [self.modelArray objectAtIndex:currentIndex + 1];
    return cVC;
}

The code in your comment:

- (IBAction)newButtonTapped:(id)sender 
{ 
    if(sender){ 
        [self.modelArray addObject:@"Page Two"]; 
        ContentVC *newVC = [[ContentVC alloc] initWithNibName:@"ContentVC" bundle:nil]; 
        NSArray *arr = [NSArray arrayWithObject:newVC]; 
        [self.pageVC setViewControllers:arr direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; 
        NSLog(@"%@", self.modelArray); 
    } 
}

doesn't work because you didn't set the labelContents of the ContentVC. So you basically end up calling [self.modelObject indexOfObject:nil] or [self.modelObject indexOfObject:@""] depending on your implementation of labelContents. Since neither of them is in the array, the call returns NSNotFound which on 64bit systems is translated to NSIntegerMax and that is 2147483647. Later you try to access your modelArray at index NSIntegerMax - 1 and that raises an NSRangeException.

So can fix that by either setting the labelContents in your newButtonTapped: method or if you follow my suggestion to redesign your code:

- (IBAction)newButtonTapped:(id)sender 
{ 
    if(sender){ 
        ContentVC *newVC = [self contentViewControllerWithLabelContents:@"Page Two"]; 
        [self.modelArray addObject:newVC];

        NSArray *newVCs = nil;

        if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation) || self.modelArray.count == 1)
            newVCs = @[newVC];
        else
        {
            NSUInteger secondLastIndex = self.modelArray.count - 2;
            id previousVC = [self.modelArray objectAtIndex:secondLastIndex];
            newVCs = @[previousVC, newVC];
        }

        [self.pageVC setViewControllers:newVCs direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
    } 
}
like image 137
Tobi Avatar answered Feb 02 '23 00:02

Tobi