35

I am trying to disable the pan gesture recognizer for a UIPageViewController.

On iOS 5 I can loop through them and disable them.

for (UIGestureRecognizer* recognizer in self.pageViewController.gestureRecognizers) {
    if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
        recognizer.enabled = NO;
    }
}

On iOS 6 using UIPageViewControllerTransitionStyleScroll there are no gesture recognizers returned by the Page View Controller.

Clarification

This can be boiled down to:

self.pageViewController.gestureRecognizers = 0 when UIPageViewController's transition style is set to scroll so I can't access the gesture recognizers.

Is there any way I can get around this? I don't think I am doing anything wrong since the curl transition works fine.

bjtitus
  • 4,231
  • 1
  • 27
  • 35
  • I'm seeing exactly the same problem. Would love a workaround. – algal Oct 30 '12 at 22:16
  • Maybe iOS6 isnt using UIPanGestureRecognizer on UIPageVC. Have you try insert your own gesture recognizer and somehow force it to over-ride the default swipe/pan behaviour? – dklt Nov 08 '12 at 08:05
  • this still works on iOS 6 if your transition style is set to UIPageViewControllerTransitionStylePageCurl – Mona Aug 30 '13 at 23:38
  • Easier way here -> http://stackoverflow.com/questions/13373531/uipageviewcontroller-disable-scrolling – kokluch Jan 29 '14 at 10:42

12 Answers12

32

Found this in UIPageViewController.h:

// Only populated if transition style is 'UIPageViewControllerTransitionStylePageCurl'. @property(nonatomic, readonly) NSArray *gestureRecognizers;

So, not a bug - by design the pageViewController doesn't get gesture recognizers when scroll style is set.

leanne
  • 7,940
  • 48
  • 77
  • Good catch. Obviously should have looked in the header. I've awarded the answer to Sergio since he did respond with a reasonable (albeit, hacky) workaround. Obviously, this is a missing feature, though. – bjtitus Nov 29 '12 at 17:53
  • 2
    @leanne: +1 for spotting this. The question arises: is the header file or the reference doc the "real" reference? Apple could at least add that notice to its documentation to spare people some headache... – sergio Nov 29 '12 at 19:06
  • @sergio: considering that some languages don't even provide nice docs like we get from Apple, I'd think the header file is the "real" reference. I do agree that it would make things much easier if this info were included in the reference doc, since it IS provided (and appreciated very much!). – leanne Feb 22 '13 at 19:54
  • This is pretty crazy behaviour IMO and I tend to agree with @sergio (though a good catch either way @leanne). A quick peek through `updated` header files shows that the behaviour still holds for newer versions of iOS – NSTJ Aug 03 '14 at 01:21
  • 1
    I think a reason for this may be that for scrolling UIPageViewControllers Apple uses a UIScrollView. If you try to set the delegate for a UIScrollView panGestureRecognizer you will get the exception: 'NSInvalidArgumentException', reason: 'UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.' I guess they chose to not expose the gesture recognizers for this reason. – Werner Altewischer Dec 01 '14 at 14:40
28

You can always try to disable user interaction on the page view controller's sub view:

for (UIScrollView *view in self.pageViewController.view.subviews) {
    if ([view isKindOfClass:[UIScrollView class]]) {
        view.scrollEnabled = NO;
    }
}
Stunner
  • 12,025
  • 12
  • 86
  • 145
dstulic
  • 339
  • 4
  • 4
26

There is a bug filed in radar for this behavior. So, I bet that until Apple fixes it there will be no chance to solve this.

One workaround that comes to my mind is laying a transparent subview on top of your UIPageViewController and add to it a UIPanGestureRecognizer to intercept that kind of gesture and not forward further. You could enable this view/recognizer when disabling the gesture is required.

I tried it with a combination of Pan and Tap gesture recognizers and it works.

This is my test code:

- (void)viewDidLoad {
  [super viewDidLoad];

   UIPanGestureRecognizer* g1 = [[[UIPanGestureRecognizer alloc] initWithTarget:self
                                                                      action:@selector(g1Pan:)] autorelease];
  [self.view addGestureRecognizer:g1];

  UITapGestureRecognizer* s1 = [[[UITapGestureRecognizer alloc] initWithTarget:self
                                                                      action:@selector(g1Tap:)] autorelease];

  [self.view addGestureRecognizer:s1];

  UIView* anotherView = [[[UIView alloc]initWithFrame:self.view.bounds] autorelease];
  [self.view addSubview:anotherView];

  UIPanGestureRecognizer* g2 = [[[UIPanGestureRecognizer alloc] initWithTarget:self
                                                                      action:@selector(g2Pan:)] autorelease];
  [anotherView addGestureRecognizer:g2];

}

When g2 is enabled, it will prevent g1 from being recognized. On the other hand, it will not prevent s1 from being recognized.

I understand this is hack, but in the face of a seeming bug in UIPageViewController (at least, actual behavior is blatantly different from what the reference states), I cannot see any better solution.

sergio
  • 68,819
  • 11
  • 102
  • 123
  • Looks like leanne is right and gesture recognizers are only provided for UIPageViewControllerTransitionStylePageCurl. So it's more of a feature request than a bug. I will probably use a similar workaround to what you suggested in the meantime. – bjtitus Nov 29 '12 at 17:54
  • 1
    You can get more or less the same affect by just adding a gesture recognizer to the currently suggested page. With a pan gesture recognizer eating the pans, the page view controller never gets them. Just a bit easier. – David H Nov 13 '13 at 18:52
  • Strange, this works in 95% of all my test cases. But sometimes the underlying gesture passes through anyway. Maybe a bug, and its random. – Zeezer Dec 05 '13 at 13:00
  • Frustrating that something can be documented as having one behaviour everywhere except for the header files – NSTJ Aug 03 '14 at 01:21
19

According to the UIPageViewController header file, when the datasource is nil, gesture-driven navigation is disabled.

So, set datasource to nil when you want to disable swiping, then when you want to enable swiping, reset the data source.

i.e.

// turns off paging
pageViewController.datasource = nil

// turns on paging
pageViewController.datasource = self;
3rdFunkyBot
  • 396
  • 3
  • 8
  • 3
    This is definitely the most elegant solution with the least mucking about and taking advantage of stated behaviour in Apple's official API documentation. – Gabriel Dec 06 '13 at 01:58
  • This did not work in my case. I had a textfield on screen. When the keyboard is shown, I wanted to lock the screen. However setting the PVC to nil seems also to disable [textfield resignFirstResponder]; So instead, I went with dStolic's answer about setting each PVC's (scrollview)subview.scrollEnabled = NO; – ObjectiveTC Jul 16 '14 at 01:14
9

You can access the UIPanGestureRecognizer via the UIScrollview from the UIPageViewController.

for (UIView *view in self.pageController.view.subviews) {
    if ([view isKindOfClass:[UIScrollView class]])
    {
        UIScrollView *scrollView = (UIScrollView *)view;

        UIPanGestureRecognizer* panGestureRecognizer = scrollView.panGestureRecognizer;
        [panGestureRecognizer addTarget:self action:@selector(move:)];
    }
}
Berni
  • 449
  • 4
  • 5
2

If the UIPageViewControllers UIPanGestureRecognizer is eating / swallowing all events away from an other PanGestureRecognizer (eg. a sliding menu).

You can easily expand on Bernies solution and make the UIScrollViews PanGestureRecognizer require the other Recognizer to fail. Something like this:

for (UIView *view in pageViewController.view.subviews) {
    if ([view isKindOfClass:[UIScrollView class]]){
        UIScrollView *scrollView = (UIScrollView *)view;

        [scrollView.panGestureRecognizer requireGestureRecognizerToFail:otherRecognizer];
    }
}

This way the scrolling PanGestureRecognizer only fires in the areas I intended him to.

This might not be the best future prove solution since we require Apple not to change the UIPageViewControllers internal use of an UIScrollView. but...

schmidiii
  • 316
  • 2
  • 13
1

Assuming you can find the gesture recognizers and remove them is very brittle. You are using assumed knowledge of how Apple uses implements UIPageViewController to provide functionality in your app. If this changes (such as between iOS 5 and iOS 6) then you're code app will start to behave in unexpected ways. It's almost like using a private API - you've no guarantee it will work with the next OS release.

Michael
  • 1,213
  • 6
  • 9
  • 1
    This would be a good answer except that the documentation says the following about UIPageViewController's gestureRecognizers property: "These gesture recognizers are initially attached to a view in the page view controller’s hierarchy. To change the region of the screen in which the user can navigate using gestures, they can be placed on another view." While it may not be wise to remove them, they are definitely meant to be retrieved and possibly moved. – bjtitus Jan 23 '13 at 23:44
  • I didn't know that but if it is as you say, then yes, clearly Apple are giving you that control and an assurance it will remain available between OS versions (or be declared deprecated for a period before being removed). Good catch. – Michael Mar 10 '13 at 14:51
1

What happens if you use KVC (Key Value Coding) methods to access the recognizers? (I'm not where I can test this at the moment.)

A quick test might be to retrieve the number of gestureRecognizers:

[self.pageViewController countOfKey:@"gestureRecognizers"];

If that works, you might go further to retrieve the array of recognizers:

NSArray *recognizers = [self.pageViewController
                        mutableArrayValueForKey:@"gestureRecognizers"];

Thin, but maybe...

Edit: was able to finally test. Used the following:

NSArray *pvcGRsNoKVC = [[NSArray alloc]
                       initWithArray:self.pageViewController.gestureRecognizers];
NSArray *viewGRsNoKVC = [[NSArray alloc] initWithArray:self.view.gestureRecognizers];

NSArray *pvcGRsKVC = [[NSArray alloc]
                     initWithArray:[self.pageViewController valueForKey:@"gestureRecognizers"]];
NSArray *viewGRsKVC = [[NSArray alloc]
                      initWithArray:[self.view valueForKey:@"gestureRecognizers"]];

It didn't make any difference. The curl style worked fine both ways; the scroll style showed the arrays as not nil, but empty. An interesting thing, though, was that the view ALSO did not give up its recognizers - though the scroll functionality is there, so it must have at least a pan recognizer...

leanne
  • 7,940
  • 48
  • 77
0

Another solution, just for history. You can make any view to be some gesture recognizer breaker and it should work in that view's rectangle. There must be another UIPanGestureRecognizer with delegate. It can be any object with one method:

static UIPageViewController* getPageViewControllerFromView(UIView* v) {
    UIResponder* r = v.nextResponder;
    while (r) {
        if ([r isKindOfClass:UIPageViewController.class])
            return (UIPageViewController*)r;
        r = r.nextResponder;
    }
    return nil;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (getPageViewControllerFromView(otherGestureRecognizer.view))
    {
        otherGestureRecognizer.enabled = NO;
        otherGestureRecognizer.enabled = YES;
    }
    return NO;
}

You can use following class for recognizer breaking purposes:

@interface GestureRecognizerBreaker : NSObject <UIGestureRecognizerDelegate>
{
    UIGestureRecognizer* breaker_;
    BOOL(^needsBreak_)(UIGestureRecognizer*);
}

- (id) initWithBreakerClass:(Class)recognizerClass
                    checker:(BOOL(^)(UIGestureRecognizer* recognizer))needsBreak;

- (void) lockForView:(UIView*)view;
- (void) unlockForView:(UIView*)view;
@end

@implementation GestureRecognizerBreaker
- (void) dummy:(id)r {}
- (id) initWithBreakerClass:(Class)recognizerClass checker:(BOOL(^)(UIGestureRecognizer*))needsBreak {
    self = [super init];
    if (!self)
        return nil;
    NSParameterAssert([recognizerClass isSubclassOfClass:UIGestureRecognizer.class] && needsBreak);
    needsBreak_ = needsBreak;
    breaker_ = [[recognizerClass alloc] initWithTarget:self action:@selector(dummy:)];
    breaker_.delegate = self;
    return self;
}

- (BOOL) gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer {
    if (needsBreak_(otherGestureRecognizer)) {
        otherGestureRecognizer.enabled = NO;
        otherGestureRecognizer.enabled = YES;
    }
    return NO;
}

- (void) lockForView:(UIView*)view {
    [view addGestureRecognizer:breaker_];
}

- (void) unlockForView:(UIView*)view {
    [view removeGestureRecognizer:breaker_];
}
@end

This is works for example as singletone:

static GestureRecognizerBreaker* PageViewControllerLocker() {
    static GestureRecognizerBreaker* i = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        i = [[GestureRecognizerBreaker alloc]
            initWithBreakerClass:UIPanGestureRecognizer.class
            checker:^BOOL(UIGestureRecognizer* recognizer) {
                UIView* v = recognizer.view;
                UIResponder* r = v.nextResponder;
                while (r) {
                    if ([r isKindOfClass:UIPageViewController.class])
                        return YES;
                    r = r.nextResponder;
                }
                return NO;
            }];
    });
    return i;
}

After calling -lockForView: page controller's gesture doesn't work on dragging in view's frame. For example I want to lock whole space of my view controller. So at some point in view controller method I call

[PageViewControllerLocker() lockForView:self.view];

And at another point

[PageViewControllerLocker() unlockForView:self.view];
0

Inside the pageviewcontroller there is a subview called quieingscrollview it has 3 gesture recognizers which controls the page view controller to get them use

[[pageViewController.view.subviews objectAtIndex:0] gestureRecognizers]
user2568379
  • 163
  • 1
  • 1
  • 5
0

In my case, I had a UIView "Info Panel" that came down over my UIPageViewController, but the page view controller's gesture recognizers interfered with the navigation through my info panel.

My solution was to set the dataSource to nil, but also to not allow the page view controller to update focus while the info panel is up:

- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context
{
    if (self.infoPanel) {
        return NO;
    } else {
        return YES;
    }
}
Danny
  • 397
  • 3
  • 9
-1

You could use the delegate methods of UIGestureRecognizer to catch and disable any gestures For example, you could use this delegate callback:gestureRecognizer:shouldReceiveTouch:. Just make sure to set the delegate for all the recognizers.

Mundi
  • 79,884
  • 17
  • 117
  • 140
  • The problem is not that disabling the gesture recognizers doesn't work but that I can't get access to the gesture recognizer because they aren't being returned from a UIPageViewController with the scroll transition style. – bjtitus Oct 28 '12 at 02:23