8
votes

This has only started happening with ios6, but if you start a new project using the page view controller template. Then in

PCRootViewControlle::viewDidLoad()

add the lines to the bottom of the method.

for (UIGestureRecognizer *gR in self.pageViewController.gestureRecognizers)
{
    gR.delegate = self;
}

You'll need to assign the viewController so it conforms to the UIGestureRecognizerDelegate and implement the method

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch (UITouch *)touch
{
    return YES;
}

Now if you run the app and try to turn the page beyond the bounds, i.e. go to January and try to turn back so

  • (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController

returns nil.

The app will then crash.

This did not happen with ios5. I need to assign the gestureRecognizer delegate to my viewController because I do not always want the pageViewController to handle the touch events.

Has any else experienced this or point out If I am doing something wrong?

Many Thanks Stewart.

4
the error log... Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'The number of view controllers provided (0) doesn't match the number required (2) for the requested transition'noRema
I had the same issue and wrote my solution here. stackoverflow.com/questions/12565400/…inamiy
I posted an answer to this same issue here: stackoverflow.com/a/12573631/1655630rrrus
check out my answer below, it avoids infinite turning of the same pagenoRema

4 Answers

8
votes

Finally found a solution to my problem which has caused me grief so hopefully this can help someone else.

The issue is if you set the pageViewControllers delegate to your viewController

for (UIGestureRecognizer *gR in self.pageController.view.gestureRecognizers) 
{
    if ([gR isKindOfClass:[UITapGestureRecognizer class]])
    {
        gR.enabled = NO;
    }
    else if ([gR isKindOfClass:[UIPanGestureRecognizer class]])
    {
        gR.delegate = self;
    }
}

then returning nil from

pageViewController:viewControllerAfterViewController:

will crash!! only in iOS6!!

My issue was that I needed to set the delegate of the gestureRecognisers because I needed to intercept the panGesture in some situations, i.e not allowing the user to turn a page wilst touching certain parts of it due to some buttons there.

The solution is to put the logic from

pageViewController:viewControllerAfterViewController: 

into

gestureRecognizer:shouldReceiveTouch:

because as long as we return NO from that then it wont go on to call

pageViewController:viewControllerAfterViewController:

and so no need to return nil and get a crash.

However, this didn't work on the first and last page in the sequence. For example on the first page, you want to allow the page to turn forward but not back. So I thought about looking at the GestureRecogniser passed in, casting it to a PanGesture, and then checking the velocity on this, if the velocity signifies turning back ( > 0.0f ) then just return NO. This sounded good but the velocity was always zero.

I then found a very helpful little function on the GestureRecognizer delegate called:

gestureRecognizerShouldBegin:gestureRecognizer

this function is called after

gestureRecognizer:shouldReceiveTouch:

but this time the velocity from the gesture is as I expected, so I can check the velocity and only return YES for the first page if it is > 0.0f

7
votes

I believe I've found a solution that's much less convoluted than refactoring any existing pageViewController code, as noRema mentioned, or messing with selector forwarding, as mentioned here, though I would never have found it without both of those solutions as reference.

This should easily drop into any UIPageViewController project with only minor adjustments.

To implement, you leave your code as-is; all existing logic should remain the same as this only affects UIGestureRecognizer Delegate behavior. Override the gesture delegate method - (BOOL)gestureRecognizerShouldBegin: with something like the following:

NOTE: I'm using Apple's Page-Based Application code as a starting point, so refer to that if the terms _modelController and _pageViewController cause any confusion.

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{

    //Make sure we're not trying to turn backward past the first page:
    if ([_modelController indexOfViewController:[_pageViewController.viewControllers objectAtIndex:0]] == 0) {
        if ([(UIPanGestureRecognizer*)gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] &&
            [(UIPanGestureRecognizer*)gestureRecognizer velocityInView:gestureRecognizer.view].x > 0.0f) {
            NSLog(@"DENIED SWIPE PREVIOUS ON FIRST PAGE");
            return NO;
        }
        if ([(UITapGestureRecognizer*)gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] &&
            [(UITapGestureRecognizer*)gestureRecognizer locationInView:gestureRecognizer.view].x < self.view.frame.size.width/2) {
            NSLog(@"DENIED TAP PREVIOUS ON FIRST PAGE");
            return NO;
        }
    }

    //Make sure we're not trying to turn forward past the last page:
    int finalVCindexSubtractor;
    if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
        // the vc we compare is a different distance from the end based on our orientation:
        finalVCindexSubtractor = 2;
    } else {
        finalVCindexSubtractor = 1;
    }
    if ([_modelController indexOfViewController:[_pageViewController.viewControllers objectAtIndex:0]] == _modelController.pageData.count-finalVCindexSubtractor) {
        if ([(UIPanGestureRecognizer*)gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] &&
            [(UIPanGestureRecognizer*)gestureRecognizer velocityInView:gestureRecognizer.view].x < 0.0f) {
            NSLog(@"DENIED SWIPE NEXT ON LAST PAGE");
            return NO;
        }
        if ([(UITapGestureRecognizer*)gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] &&
            [(UITapGestureRecognizer*)gestureRecognizer locationInView:gestureRecognizer.view].x > self.view.frame.size.width/2) {
            NSLog(@"DENIED TAP NEXT ON LAST PAGE");
            return NO;
        }
    }
    return YES;
}
4
votes

While investigating issue with Pan Gesture in UIPageViewController under iOS 6 I have noticed that it could invoke before/after view controller methods several times within one gesture. That is bad in my particular case, as I need to wait until animation is finished for each page turn to be able to navigate to next/previous page. Thus our methods have such a structure:

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
    if (animationInProgress)
        return nil;
    UIViewController *result = nil;
    //do stuff to init result controller for next/prev page
    if (result)
        animationInProgress = YES;
    return result;
}

Same for viewControllerAfterViewController method.

And after animation finishes, this method of UIPageViewControllerDelegate is invoked:

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
        animationInProgress = NO;
        //do other stuff to handle finishing animation.
}

As PanGesture was invoked several times, then viewControllerAfterViewController and another method was invoked several times due to that, and unfortunately didFinishAnimating method was not invoked in this case, which lead to animationInProgress flag set to YES always and user was not able to navigate through pages at all. NOTE: happens only in iOS 6.

To fix that issue we had to handle PanGesture somehow. Using UIView gestureRecognizerShouldBegin:gestureRecognizer (which was added in iOS 6) did not solve an issue at all, setting controller as delegate for gesture lead to crashes when nil is returned from before/after methods. Thus we tried to add logics to not invoke before/after controller methods in cases when PanGesture is in UIGestureRecognizerStateChanged state (that was investigated based on some testing to determine what is gesture state when it invokes methods in loop).

By adding method like:

- (BOOL)skipIfPanGestureInProgress {
    BOOL shouldSkip = NO;
    if(pvcPanGestureRecognizer && [pvcPanGestureRecognizer state] == UIGestureRecognizerStateChanged){
        animationInProgress = NO;
        shouldSkip = YES;
    }
    return shouldSkip;
}

After that methods look like:

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
    if([self skipIfPanGestureInProgress]){
        return nil;
    }
    if (animationInProgress)
        return nil;
    UIViewController *result = nil;
    //do stuff to init result controller for next/prev page
    if (result)
        animationInProgress = YES;
    return result;
}

This helped to solve loop problem with animationInProgress flag.

Hope this will be helpful for someone.

2
votes

I had the same issue with UIPageViewController crashing with iOS6 with the same error when navigating beyond the bounds.

None of the above solutions worked for me but I eventually found that moving the following line from viewDidLoad to viewDidAppear fixed it completely.

self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;