UIPageViewController Gesture recognizers

52,648

Solution 1

You can override

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
 shouldReceiveTouch:(UITouch *)touch

to better control when the PageViewController should receive the touch and not. Look at "Preventing Gesture Recognizers from Analyzing Touches" in Dev API Gesture Recognizers

My solution looks like this in the RootViewController for the UIPageViewController:

In viewDidLoad:

//EDITED Need to take care of all gestureRecogizers. Got a bug when only setting the delegate for Tap
for (UIGestureRecognizer *gR in self.view.gestureRecognizers) {
    gR.delegate = self;
}

The override:

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    //Touch gestures below top bar should not make the page turn.
    //EDITED Check for only Tap here instead.
    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        CGPoint touchPoint = [touch locationInView:self.view];
        if (touchPoint.y > 40) {
            return NO;
        }
        else if (touchPoint.x > 50 && touchPoint.x < 430) {//Let the buttons in the middle of the top bar receive the touch
            return NO;
        }
    }
    return YES;
}

And don't forget to set the RootViewController as UIGestureRecognizerDelegate.

(FYI, I'm only in Landscape mode.)

EDIT - The above code translated into Swift 2:

In viewDidLoad:

for gr in self.view.gestureRecognizers! {
    gr.delegate = self
}

Make the page view controller inherit UIGestureRecognizerDelegate then add:

func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
    if let _ = gestureRecognizer as? UITapGestureRecognizer {
        let touchPoint = touch .locationInView(self.view)
        if (touchPoint.y > 40 ){
            return false
        }else{
            return true
        }
    }
    return true
}

Solution 2

Here is another solution, which can be added in the viewDidLoad template right after the self.view.gestureRecognizers = self.pageViewController.gestureRecognizers part from the Xcode template. It avoids messing with the guts of the gesture recognizers or dealing with its delegates. It just removes the tap gesture recognizer from the views, leaving only the swipe recognizer.

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

// Find the tap gesture recognizer so we can remove it!
UIGestureRecognizer* tapRecognizer = nil;    
for (UIGestureRecognizer* recognizer in self.pageViewController.gestureRecognizers) {
    if ( [recognizer isKindOfClass:[UITapGestureRecognizer class]] ) {
        tapRecognizer = recognizer;
        break;
    }
}

if ( tapRecognizer ) {
    [self.view removeGestureRecognizer:tapRecognizer];
    [self.pageViewController.view removeGestureRecognizer:tapRecognizer];
}

Now to switch between pages, you have to swipe. Taps now only work on your controls on top of the page view (which is what I was after).

Solution 3

I had the same problem. The sample and documentation does this in loadView or viewDidLoad:

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

This replaces the gesture recognizers from the UIViewControllers views with the gestureRecognizers of the UIPageViewController. Now when a touch occurs, they are first sent through the pageViewControllers gesture recognizers - if they do not match, they are sent to the subviews.

Just uncomment that line, and everything is working as expected.

Phillip

Solution 4

In newer versions (I am in Xcode 7.3 targeting iOS 8.1+), none of these solutions seem to work.

The accepted answer would crash with error:

UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.

The currently highest ranking answer (from Pat McG) no longer works as well because UIPageViewController's scrollview seems to be using odd gesture recognizer sub classes that you can't check for. Therefore, the statement if ( [recognizer isKindOfClass:[UITapGestureRecognizer class]] ) never executes.

I chose to just set cancelsTouchesInView on each recognizer to false, which allows subviews of the UIPageViewController to receive touches as well.

In viewDidLoad:

guard let recognizers = self.pageViewController.view.subviews[0].gestureRecognizers else {
    print("No gesture recognizers on scrollview.")
    return
}

for recognizer in recognizers {
    recognizer.cancelsTouchesInView = false
}

Solution 5

Setting the gestureRecognizers delegate to a viewController as below no longer work on ios6

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

In ios6, setting your pageViewController's gestureRecognizers delegate to a viewController causes a crash

Share:
52,648
Rich86man
Author by

Rich86man

Updated on February 05, 2020

Comments

  • Rich86man
    Rich86man about 4 years

    I have a UIPageViewController load with my Viewcontroller.

    The view controllers have buttons which are overridden by the PageViewControllers gesture recognizers.

    For example I have a button on the right side of the viewcontroller and when you press the button, the PageViewController takes over and changes the page.

    How can I make the button receive the touch and cancel the gesture recognizer in the PageViewController?

    I think the PageViewController makes my ViewController a subview of its view.

    I know I could turn off all of the Gestures, but this isn't the effect I'm looking for.

    I would prefer not to subclass the PageViewController as apple says this class is not meant to be subclassed.

  • isaac
    isaac over 12 years
    Sataym, are you suggesting that it's impossible to place buttons or interactive elements in a UIPageViewController? And Apple just forgot to mention that in all the UIPageViewController documentation?
  • Satyam
    Satyam over 12 years
    No Look at Philips answer. Until and unless we comment that line of code, none of the controls will receive touch events unless it is first view. After commenting that line of code, it can be any custom button or label or UIButton or something else will receive touch events.
  • SpaceTrucker
    SpaceTrucker over 12 years
    I've ran in this too. I do have a button in the bottom middle of the page. That seems to work. but putting it on the left or right won't.
  • Giorgio Barchiesi
    Giorgio Barchiesi over 12 years
    Thank you, this solved my problem. In my case I wanted the tapping gesture to be in my full control, regardless of the touch position, so the code was even simpler: it just checks whether the gestureRecognizer is kind of class UITapGestureRecognizer.
  • DonnaLea
    DonnaLea almost 12 years
    I like that this method doesn't mess with setting the delegate of the gesture recognizers.
  • Kof
    Kof almost 12 years
    There seems to be a problem with this solution, at least with my case. Once I've changed the delegate of the gesture recognizer, I started getting NSInvalidArgumentException every time viewControllerBeforeViewController or viewControllerAfterViewController returned nil (indicated last page). Exception reason: The number of view controllers provided (0) doesn't match the number required (2) for the requested transition.
  • Chintan Patel
    Chintan Patel over 11 years
    Much better solution. However, i ended up enabling and disabling tapRecognizer as i again needed to enable the tap gesture after a sub view is dismissed.
  • Basem Saadawy
    Basem Saadawy over 11 years
    Very simple. I prefer it. Thank you.
  • Rob
    Rob over 11 years
    Isn't the whole point of responder chains that you can have handlers and they can do something or not? That you don't have to just have one way to do something? I'd like to see a solution where the tap on the far right of the page is ignored by the controls (or toolbar), and sent up the chain so it can still trigger paging. [That's why responders do Chain of Responsibility; this solution just sets that design aside.]
  • DiscDev
    DiscDev about 11 years
    Great solution and easy to understand. This should be the accepted answer. Note that this was only an issue for my users on iOS 5.x. On 6.x the buttons on the right side of the UIPageViewController received touch events normally and did not advance to the next page. Once I added this code my buttons worked properly on all iOS versions that I support (> 5.0)
  • amergin
    amergin almost 11 years
    @Kof the NSInvalidArgumentExceptions you're getting seem to occur only on iOS 6 when using the pagecurl transition. It can be avoided by returning [self viewControllerAtIndex:0] instead of nil (for viewControllerBeforeViewController) and [self viewControllerAtIndex:maxpages] instead of nil (for viewControllerAfterViewController) - an unfortunate side effect is the additional page curl animation.
  • Septiadi Agus
    Septiadi Agus over 10 years
    what template? What template are you talking about?
  • Septiadi Agus
    Septiadi Agus over 10 years
    self.pageViewController.gestureRecognizers is empty if we uses UIPageViewControllerTransitionStyleScroll
  • Pat McG
    Pat McG over 10 years
    The template in question is the RootViewController class generated by Xcode if you create a new Page-Based Application.
  • beryllium
    beryllium over 10 years
    In iOS6 _pageViewController.gestureRecognizers (scroll effect) is empty array and you can't handle those recognizers.
  • Bill Cheswick
    Bill Cheswick almost 10 years
    I create pageviewcontrollers regularly as my user jumps, curls, and slides to various different page views. In the routine that creates a new pageviewcontroller, I use a slightly simpler version of the excellent code shown above:
  • Bill Cheswick
    Bill Cheswick almost 10 years
    I create pageviewcontrollers regularly as my user jumps, curls, and slides to various different page views. In the routine that creates a new pageviewcontroller, I use a slightly simpler version of the excellent code shown above:- (UIPageViewController *) newPageController: (PageVC *)newPageVC {
  • Asela Liyanage
    Asela Liyanage over 4 years
    Didn't match a gesture for me on iOS 12, Swift 4.2
  • David
    David over 4 years
    This is the simplest solution, but instead of enabled use isUserInteractionEnabled