56

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.

Fattie
  • 27,874
  • 70
  • 431
  • 719
Rich86man
  • 6,507
  • 2
  • 26
  • 27
  • I also have been working on app that uses PageViewController ... and I'm looking for a way to change pages programmaticly ... can you help me a bit on this one ? – Toncean Cosmin Nov 11 '11 at 09:04
  • 2
    sure. To change a page programmatically you want to do this : `NSArray *viewControllers = [NSArray arrayWithObject:pageIWantToTurnTo];` then `[pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];` – Rich86man Nov 22 '11 at 17:26
  • Any example the completion is not NULL? – Bagusflyer Aug 07 '12 at 04:31
  • I don't understand. Are you looking for the final product? – Rich86man Aug 07 '12 at 18:31
  • @Rich86man Can you tell me how to show more than one view controller in page view controller. I am using UIPageViewController, but it is repeating same views again. CAn you help me out in this. – iPhone Programmatically Nov 21 '12 at 06:40
  • Here is my question with specification "http://stackoverflow.com/questions/13415198/accessing-multiple-view-controllers-in-page-controller". – iPhone Programmatically Nov 21 '12 at 06:42
  • FYI - on iOS 6.x this doesn't seem to be an issue. I have buttons on the right side of my UIPageViewController and they receive the tap just fine. On iOS 5.1.1 I have users complaining that tapping on the button on the right turns the page, just like the issue described here. – DiscDev Mar 26 '13 at 14:16
  • `This class is generally used as-is, but can also be subclassed.` from: https://developer.apple.com/documentation/uikit/uipageviewcontroller – Samuël Dec 29 '22 at 08:49

15 Answers15

56

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).

Pat McG
  • 706
  • 5
  • 7
  • 2
    I like that this method doesn't mess with setting the delegate of the gesture recognizers. – DonnaLea Jun 14 '12 at 03:30
  • 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. – Chintan Patel Aug 31 '12 at 07:10
  • Very simple. I prefer it. Thank you. – Basem Saadawy Nov 07 '12 at 14:30
  • 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.] – Rob Jan 11 '13 at 16:27
  • 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) – DiscDev Mar 26 '13 at 18:36
  • what template? What template are you talking about? – Septiadi Agus Aug 01 '13 at 05:46
  • 12
    self.pageViewController.gestureRecognizers is empty if we uses UIPageViewControllerTransitionStyleScroll – Septiadi Agus Aug 01 '13 at 07:17
  • The template in question is the RootViewController class generated by Xcode if you create a new Page-Based Application. – Pat McG Aug 07 '13 at 02:50
  • 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 Jun 29 '14 at 16:54
  • 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 { – Bill Cheswick Jun 29 '14 at 16:55
50

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
}
Aamir
  • 16,329
  • 10
  • 59
  • 65
jramer
  • 664
  • 6
  • 8
  • 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. – Giorgio Barchiesi Dec 20 '11 at 09:19
  • 13
    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`. – Kof Jul 06 '12 at 08:58
  • @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. – amergin Jul 12 '13 at 13:23
  • 17
    In iOS6 `_pageViewController.gestureRecognizers` (scroll effect) is empty array and you can't handle those recognizers. – beryllium Nov 01 '13 at 10:52
4

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

TMin
  • 2,280
  • 1
  • 25
  • 34
Phillip
  • 41
  • 1
3

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

Community
  • 1
  • 1
noRema
  • 559
  • 5
  • 13
3

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
}
Hunter Monk
  • 1,967
  • 1
  • 14
  • 25
2

I used

for (UIScrollView *view in _pageViewController.view.subviews) {
    if ([view isKindOfClass:[UIScrollView class]]) {
        view.delaysContentTouches = NO;
    }
}

to allow clicks to go through to buttons inside a UIPageViewController

Adam Johns
  • 35,397
  • 25
  • 123
  • 176
2

In my case I wanted to disable tapping on the UIPageControl and let tapping being received by another button on the screen. Swipe still works. I have tried numerous ways and I believe that was the simplest working solution:

for (UIPageControl *view in _pageController.view.subviews) {
    if ([view isKindOfClass:[UIPageControl class]]) {
        view.enabled = NO;
    }
}

This is getting the UIPageControl view from the UIPageController subviews and disabling user interaction.

1

Just create a subview (linked to a new IBOutlet gesturesView) in your RootViewController and assign the gestures to this new view. This view cover the part of the screen you want the gesture enable.

in viewDidLoad change :

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

to :

self.gesturesView.gestureRecognizers = self.pageViewController.gestureRecognizers;
Kampai
  • 22,848
  • 21
  • 95
  • 95
tdf
  • 106
  • 1
  • 2
0

Also can use this (thanks for help, with say about delegate):

// add UIGestureRecognizerDelegate

NSPredicate *tp = [NSPredicate predicateWithFormat:@"self isKindOfClass: %@", [UITapGestureRecognizer class]];
UITapGestureRecognizer *tgr = (UITapGestureRecognizer *)[self.pageViewController.view.gestureRecognizers filteredArrayUsingPredicate:tp][0];
tgr.delegate = self; // tap delegating

NSPredicate *pp = [NSPredicate predicateWithFormat:@"self isKindOfClass: %@", [UIPanGestureRecognizer class]];
UIPanGestureRecognizer *pgr = (UIPanGestureRecognizer *)[self.pageViewController.view.gestureRecognizers filteredArrayUsingPredicate:pp][0];
pgr.delegate = self; // pan delegating

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch 
{
    CGPoint touchPoint = [touch locationInView:self.view];

    if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation]) && touchPoint.y > 915 ) {
        return NO; // if y > 915 px in portrait mode
    }

    if (UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) && touchPoint.y > 680 ) {
        return NO; // if y > 680 px in landscape mode
    }

    return YES;
}

Work perfectly for me :)

iTux
  • 1,946
  • 1
  • 16
  • 20
0

This is the solution which worked best for me I tried JRAMER answer with was fine except I would get an Error when paging beyond the bounds (page -1 or page 23 for me)

PatMCG solution did not give me enough flexibility since it cancelled all the taprecognizers, I still wanted the tap but not within my label

In my UILabel I simply overrode as follows, this cancelled tap for my label only

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
 if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
     return NO;
 } else {
     return YES;
 }
}
Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
0

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 *npVC = [[UIPageViewController alloc]
                       initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl
                       navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                       options: options];
...

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

Now the taps work as I wish (with left and right taps jumping a page, and the curls work fine.

Bill Cheswick
  • 634
  • 7
  • 12
0

Swift 3 extension for removing tap recognizer:

    import UIKit

extension UIPageViewController {
    func removeTapRecognizer() {
        let gestureRecognizers = self.gestureRecognizers

        var tapGesture: UIGestureRecognizer?
        gestureRecognizers.forEach { recognizer in
            if recognizer.isKind(of: UITapGestureRecognizer.self) {
                tapGesture = recognizer
            }
        }
        if let tapGesture = tapGesture {
            self.view.removeGestureRecognizer(tapGesture)
        }
    }
}
Roman Barzyczak
  • 3,785
  • 1
  • 30
  • 44
0

I ended up here while looking for a simple, catch-all way to respond to taps on my child view controllers within a UIPageViewController. The core of my solution (Swift 4, Xcode 9) wound up being as simple as this, in my RootViewController.swift (same structure as Xcode's "Page-Based App" template):

override func viewDidLoad() {
    super.viewDidLoad()

    ...

    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(pageTapped(sender:)))
    self.pageViewController?.view.subviews[0].addGestureRecognizer(tapGesture)
}

...

@objc func pageTapped(sender: UITapGestureRecognizer) {
    print("pageTapped")
}

(I also made use of this answer to let me keep track of which page was actually tapped, ie. the current one.)

brookinc
  • 619
  • 7
  • 8
0

I worked out a working solution. Add another UIGestureRecognizer to UIPageViewController and implement delegate method provided below. In every moment that you have to resolve which gesture should be locked or passed further this method will be called. Remember to provide a reference to confictingView, which in my case it was UITableView, which also recognizes pan gesture. This view was placed inside UIPageViewController, so a pan gesture was recognized twice or just in randomly way. Now in this method, I check if pan gesture is inside both my UITableView and UIPageViewController, and I decide that UIPanGestureRecognizer is primary.

This approach doesn't override directly any of another gesture recognizers so we don't have to worry about mentioned 'NSInvalidArgumentException'.

Keep in mind that pattern actually is not approved by Apple :)

 var conflictingView:UIView?
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if otherGestureRecognizer.view === pageViewController?.view {

            if let view = conflictingView {
                var point = otherGestureRecognizer.location(in: self.view)
                if view.frame.contains(point) {
                    print("Touch in conflicting view")
                    return false
                }
            }
            print("Touch outside conficting view")
            return true
        }
        print("Another view passed out")
        return true
    }
0

If you're using a button that you've subclassed, you could override touchesBegan, touchesMoved, and touchesEnded, invoking your own programmatic page turn as appropriate but not calling super and passing the touches up the notification chain.

isaac
  • 4,867
  • 1
  • 21
  • 31
  • 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? – isaac Oct 27 '11 at 14:06
  • 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. – Satyam Oct 27 '11 at 14:25
  • 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. – SpaceTrucker Nov 02 '11 at 15:51