Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIPageViewController gesture is calling viewControllerAfter: but doesn't animate

I have a really interesting issue with UIPageViewController.

My project is set up very similarly to the example Page Based Application template. Every now and then (but reproducible to a certain extent) a certain pan gesture will call out to -(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController.

I return the viewcontroller for the next page, but a page flip animation is never ran and my delegate method is never called.

Here is the code for viewControllerAfterViewController

-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    PageDisplayViewController *vc = (PageDisplayViewController *)viewController;
    NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];
    if(index == (self.pageFetchController.fetchedObjects.count - 1)) return nil;
    return [self getViewControllerForIndex:(++index)];
}

Here is the getViewControllerForIndex:

-(PageDisplayViewController *)getViewControllerForIndex:(NSUInteger)index
{
    PageDisplayViewController *newVC = [self.storyboard instantiateViewControllerWithIdentifier:@"PageDisplayController"];
    newVC.page = [self.pageFetchController.fetchedObjects objectAtIndex:(index)];
    newVC.view.frame = CGRectMake(0, 0, 1024, 604);
    NSLog(@"%i", index);
    if(index == 0)
    {
        //We're moving to the first, animate the back button to be hidden.
        [UIView animateWithDuration:0.5 animations:^
        {
            self.backButton.alpha = 0.f;
        } completion:^(BOOL finished){
            self.backButton.hidden = YES;
        }];
    }
    else if(index == (self.pageFetchController.fetchedObjects.count - 1))
    {
        [UIView animateWithDuration:0.5 animations:^{
            self.nextButton.alpha = 0.f;
        } completion:^(BOOL finished){
            self.nextButton.hidden = YES;
        }];
    }
    else
    {
        BOOL eitherIsHidden = self.nextButton.hidden || self.backButton.hidden;
        if(eitherIsHidden)
        {
            [UIView animateWithDuration:0.5 animations:^{
                if(self.nextButton.hidden)
                {
                    self.nextButton.hidden = NO;
                    self.nextButton.alpha = 1.f;
                }
                if(self.backButton.hidden)
                {
                    self.backButton.hidden = NO;
                    self.backButton.alpha = 1.f;
                }
            }];
        }
    }
    return newVC;
}

Basically, I create the view controller, set it's data object, then fade a next/back button out depending on the index.

Delegate method

-(void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    PageDisplayViewController *vc = [previousViewControllers lastObject];
    NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];

    if (!completed)
    {
        [self.pagePreviewView setCurrentIndex:index];
        NSLog(@"Animation Did not complete, reverting pagepreview");
    }
    else
    {
        PageDisplayViewController *curr = [pageViewController.viewControllers lastObject];
        NSUInteger i = [self.pageFetchController.fetchedObjects indexOfObject:curr.page];
        [self.pagePreviewView setCurrentIndex:i];
        NSLog(@"Animation compeleted, updating pagepreview. Index: %u", i);
    }
}

I only noticed this issue because randomly, my back button would reappear on screen. After tossing some NSLog() statements in there, I notice that my dataSource method gets called for an index of 1, but no animation ever plays or delegate gets called. Whats even scarier, is that if I try to pan the next page, index 1 gets called for AGAIN.

I fear this may be a bug with the UIPageViewController.

like image 255
DavidAndroidDev Avatar asked Sep 21 '12 15:09

DavidAndroidDev


2 Answers

Please look at my other answer in the first place, this one has serious flaws but I'm leaving it here as it might still help someone.


First off, a disclaimer: The following solution is a HACK. It does work in the environment I tested but there is no guarantee that it works in yours nor that it won't be broken by the next update. So proceed with care.

TL;DR: grab the UIPanGestureRecognizer of the UIPageViewController and hijack its delegate calls but keep forwarding them to the original target.

Longer version:

My findings on the issue: the UIPageViewController shipped in iOS 6 is different in behavior to the one in iOS 5 in that it may call the pageViewController:viewControllerBeforeViewController: on its datasource even if there is no page turning going on in any sense (read: no tap, swipe, or valid direction-matching panning has been recognized). This, of course, breaks our former assumption that the before/after calls are equivalent to an "animation begin" trigger and are consistently followed by a pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted: call to the delegate. (Eventually this is a bold assumption to make but I guess I was not alone with that.)

I found out that the extra calls to the datasource are likely to happen when the default UIPanGestureRecognizer on the page view controller starts to recognize a pan gesture that in the end doesn't match the direction of the controller (e.g. vertical panning in a horizontally paging PVC). Interestingly enough, in my environment it was always the "before" method which got hit, never the "after". Others suggested interfereing with the gesture recognizer's delegate but that didn't work for me the way it was described there so I kept experimenting.

Finally I found a workaround. First we grab the pan gesture recognizer of the page view controller:

@interface MyClass () <UIGestureRecognizerDelegate>
{
    UIPanGestureRecognizer* pvcPanGestureRecognizer;
    id<UIGestureRecognizerDelegate> pvcPanGestureRecognizerDelegate;
}
...
for ( UIGestureRecognizer* recognizer in pageViewController.gestureRecognizers )
{
    if ( [recognizer isKindOfClass:[UIPanGestureRecognizer class]] )
    {
        pvcPanGestureRecognizer = (UIPanGestureRecognizer*)recognizer;
        pvcPanGestureRecognizerDelegate = pvcPanGestureRecognizer.delegate;
        pvcPanGestureRecognizer.delegate = self;
        break;
    }
}

Then we implement the UIGestureRecognizerDelegate protocol in our class:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if ( gestureRecognizer == pvcPanGestureRecognizer &&
        [pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizer:shouldReceiveTouch:)] )
    {
        return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
                                               shouldReceiveTouch:touch];
    }
    return YES;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if ( gestureRecognizer == pvcPanGestureRecognizer &&
        [pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)] )
    {
        return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
               shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
    }
    return NO;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    if ( gestureRecognizer == pvcPanGestureRecognizer &&
        [pvcPanGestureRecognizerDelegate respondsToSelector:@selector(gestureRecognizerShouldBegin:)] )
    {
        return [pvcPanGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer];
    }
    return YES;
}

Apparently, the methods don't do anything sensible, they just forward the invocations to the original delegate (making sure that that actually implements them). Still, this forwarding seems to be sufficient for the PVC to behave and not call the datasource when there is no need to.

This workaround fixed the issue for me on devices running iOS 6. Code which was compiled with the iOS 6 SDK but with a deployment target of iOS 5 had already run flawlessly on 5.x devices, so the fix is not necessary there but according to my tests it doesn't do any harm either.

Hope someone finds this useful.

like image 110
Lvsti Avatar answered Nov 08 '22 00:11

Lvsti


I have tried your solution and it came almost working, but still with some issues. The best solution came with adding method

 - (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers 

which is available starting from iOS 6 and it is required for it. If not to implement it, issues may occur with those gestures. Implementing it helped to solve major part of issues.

like image 24
Sergii N. Avatar answered Nov 08 '22 01:11

Sergii N.