How to block a gesture from superview to subview? - ios

I'm writing a module that everytime I swipe on a view, two sub views with a half size of the view will be added. Those subviews have their own gestures (eg: pan,...). The first time I swipe, it's OK because none of subview has been created. But once the subview been created, everytime I swipe, the swipe gesture is alway pass to its subviews. :(, so I have to swipe 2 times to divide.
I want to know is there any way to block swipe passing to its subview? Thank you.
UPDATE
I used shouldRecognizeSimultaneouslyWithGestureRecognizer to make those gestures work simultaneously. But there's still have some problems. The parent view have its Swipe gesture, the subview have its Pan gesture. Since I use souldRecognizeSimultaneouslyWithGestureRecognizer, sometime when I'm panning, the swipe gesture triggers. So, you know how to disable Swipe while Pan is active in this situation?

You have to implement the UIGestureRecognizerDelegate method:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
And add your controller as the delegate of the gesture recognizers. Then, when two gesture recognizers respond to a gesture, this method will be called and here you can implement the logic you want for your app.
In the interface declaration of the controller you have to type:
#interface testcViewController () <UIGestureRecognizerDelegate>
Then, when creating the gesture recognizer:
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipe)];
swipe.direction = UISwipeGestureRecognizerDirectionDown;
swipe.delegate = self;
[self.view addGestureRecognizer:swipe];
And then, finally, you add this method to the controller:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
BOOL shouldInteract = NO;
//Here you decide whether or not the two recognizers whould interact.
return shouldInteract;
}
EDIT
You can also implement
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
And here, detect if you have already presented the subviews, and block any gesture you want.

To block all gesture recognizers from superviews I created a UIGestureRecognizer sub class that will do just that when attached to a view. See the following code (taken from my WEPopover project):
#import "WEBlockingGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#implementation WEBlockingGestureRecognizer
- (id)init {
return [self initWithTarget:self action:#selector(__dummyAction)];
}
- (id)initWithTarget:(id)target action:(SEL)action {
if ((self = [super initWithTarget:target action:action])) {
self.cancelsTouchesInView = NO;
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
if (self.state == UIGestureRecognizerStatePossible) {
self.state = UIGestureRecognizerStateBegan;
}
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
self.state = UIGestureRecognizerStateRecognized;
}
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer {
return [self isGestureRecognizerAllowed:preventingGestureRecognizer];
}
- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer {
return ![self isGestureRecognizerAllowed:preventedGestureRecognizer];
}
- (BOOL)shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return ![self isGestureRecognizerAllowed:otherGestureRecognizer];
}
- (BOOL)shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return NO;
}
- (BOOL)isGestureRecognizerAllowed:(UIGestureRecognizer *)gr {
return [gr.view isDescendantOfView:self.view];
}
- (void)__dummyAction {
}
#end
If you want to block all gesture recognizers attached to parent views of some view, just do the following:
- (void)blockParentGesturesForView:(UIView *)v {
[v addGestureRecognizer:[WEBlockingGestureRecognizer new]];
}

set userinteractionEnabled to NO of your subView
subview.userinteractionEnabled=NO
if you dont want to disable userInteraction then use cancelsTouchesInView method
cancelsTouchesInView—If a gesture recognizer recognizes its gesture,
it unbinds the remaining touches of that gesture from their view (so
the window won’t deliver them). The window cancels the previously
delivered touches with a (touchesCancelled:withEvent:) message. If a
gesture recognizer doesn’t recognize its gesture, the view receives
all touches in the multi-touch sequence.

try like this,
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return NO;
}

Considering that I have a dialogView as a direct subview of my UIViewController's main view I'm attaching a gesture recognizer to the main view and do the following (setting my view controller as the gesture recognizer delegate):
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let point = touch.location(in: view)
return !dialogView.frame.contains(point)
}

Took what I read here and made a Swift 5 that works great (for me)! I have a slide control in a view that also has a swipe recognizer for moving to another view. If the finger doesn't hit the slider's thumb, then the whole view moves.
By placing this view under the slider (expanded so its somewhat bigger), misses don't do anything.
final class BlockingView: UIView, UIGestureRecognizerDelegate {
let swipe: UISwipeGestureRecognizer = UISwipeGestureRecognizer();
init() {
super.init(frame: CGRect.zero)
swipe.direction = [.left, .right]
self.addGestureRecognizer(swipe)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
#objc override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return false }
}
Hope this saves someone the trouble!

Related

How to compare the types of gestures on iOS?

I have three different gestures with two different types on one view.
First is a UITapGestureRecognizer and the two others are UILongPressGestureRecognizer.
The long press gesture recognizer have different minimumPressDuration, one is 0.15 and the other is 0.50, so to I implemented he following function so that all the gestures are recognized:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *) otherGestureRecognizer{
return true;
}
The function does allow all the gestures to be recognized but the problem is whenever a UILongPressGestureRecognizer is recognized, a UITapGestureRecognizer is also recognized.
So, I want to know how can I compare the types of gestureRecognizer in
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *) otherGestureRecognizer
or how to stop the UITapGestureRecognizer when UILongPressGestureRecognizer is detected because UITapGestureRecognizer is triggered whenever UILongPressGestureRecognizer is triggered.
Instead of returning YES to all cases in shouldRecognizeSimultaneouslyWithGestureRecognizer:, if you don't want the gestures to be recognized simultaneously, you should actually return NO:
- (BOOL) gestureRecognizer: (UIGestureRecognize *) gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *) otherGestureRecognizer {
return NO;
}
But to accomplish what you're apparently trying to accomplish, I'd recommend using a different UIGestureRecognizerDelegate method instead -- gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: -- so that you can specify which gesture is recognized before the other. In this case, since you'd like to stop the UITapGestureRecognizer when a UILongPressGestureRecognizer is detected, try this:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
// If the gesture recognizer is a UITapGestureRecongizer, but the other
// gesture detected is a UILongPressGestureRecognizer, require the
// UITapGestureRecognizer to fail.
if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] &&
[otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
return YES;
} else {
return NO;
}
}
Can check the class of the UIGestureRecognizer
For example:
-(BOOL) gestureRecognizer: (UIGestureRecognize *) gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *) otherGestureRecognizer {
if ([gestureRecognizer isMemberOfClass: [UILongPressGestureRecognizer class]]) {
//do stuff
}
//etc
}
I converted the code for Swift 5 to add to this post.
It should work as I pulled it off the Apple Documents.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer)
-> Bool {
return true
}
I'm going to try this tomorrow as I've been struggling with UIScrollView and UISwipeGestureRecognizer working together. If I disable Scrolling, then the gesture works, but if I enable Scrolling (what I need), then gesture does not work. I want this feature for hiding and showing the top Navigation Bar and the bottom TabBar when scrolling up and down my scrollview.
Then I'm going to try to get it to work with a UICollectionView on the next page in my app.
Being new to coding, any suggestions would be appreciated if the above code won't be enough.
I'll update this comment if it works or not.

Custom UIControl and Gesture Recognizer

I am trying to create a custom UIControl similar to a slider.
This control is to be the subview of a view that also has a tap gesture recognizer attached to it.
The problem now is that this tap gesture recognizer cancels the touches sent to my control. Is there a way I can override this from within the code of my control?
If I look into the standard controls in iOS it looks as if UIButton has a way of overriding the tap gesture recognizer but UISlider doesn't. So if I replace my custom control with a UIButton the tap gesture recognizer does not trigger its action, but if I replace it with a slider it does.
edit: I made a small project in Xcode to play around in. Download here https://dl.dropboxusercontent.com/u/165243/TouchConcept.zip and try to change it so that
The UICustomControl does not know about the tap gesture recognizer
The UICustomControl is not cancelled when the user taps down on the yellow box
The UICustomControl does not inherit from UIButton (that is a solution that does not feel right and might give me more headaches later on)
The code:
// inherit from UIButton will give the wanted behavior, inherit from UIView (or UIControl) gives
// touchesCancelled by the gesture recognizer
#interface UICustomControl : UIView
#end
#implementation UICustomControl
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{ NSLog(#"touchesBegan"); }
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{ NSLog(#"touchesMoved"); }
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{ NSLog(#"touchesEnded"); }
-(void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{ NSLog(#"touchesCancelled"); }
#end
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(logTap:)];
[self.view addGestureRecognizer:tapRecognizer];
UIView *interceptingView = [[UICustomControl alloc]initWithFrame:CGRectMake(10, 10, 100, 100)];
interceptingView.userInteractionEnabled = YES;
interceptingView.backgroundColor = [UIColor yellowColor];
[self.view addSubview: interceptingView];
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void) logTap: (id) sender
{
NSLog(#"gesture recognizer fired");
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
You can configure the gesture recognizer to not cancel touches in the view it's attached using the "cancels touches in view" property:
myGestureRecognizer.cancelsTouchesInView = NO;
I'm a little bit late, but for those (like me) stumbling into this question, I used an alternative solution:
Use the delegate of the gesture recogniser.
UITapGestureRecognizer *tapGestRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(dismissInfoBox:)];
tapGestRec.delegate = self;
[self.view addGestureRecognizer:tapGestRec];
Then do a sort of hit test in the shouldReceiveTouch delegate function when the gesture recogniser wants to handle/swallow a touch.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint location = [touch locationInView:self.view];
return !CGRectContainsPoint(self.myCustomControl.frame, location) && !CGRectContainsPoint(self.myOtherCustomControl.frame, location);
}
I did all this in my ViewController, this way the UIControl does not have to know about any sibling views and the gesture recogniser does not 'steal' taps from my custom controls and only handles 'uncaught' taps.
Also, this way you won't trigger both the gesture recogniser and the custom control, which would happen with cancelsTouchesInView.
BTW, maybe it works with UIButton because UIButton uses gesture recognisers internally? I think they understand each other, while UIControls and recognisers do not. Not sure though.
override gestureRecognizerShouldBegin(_:) in your UIControl subclass.
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.isKind(of: UITapGestureRecognizer.self) {
return false
} else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
}
You should follow this tutorial
http://www.raywenderlich.com/1768/uiview-tutorial-for-ios-how-to-make-a-custom-uiview-in-ios-5-a-5-star-rating-view
It shows you How to make a custom uiview.
And then do follow this one
http://www.raywenderlich.com/29474/ipad-for-iphone-developers-101-in-ios-6-custom-input-view-tutorial

Get Swipe to Delete on UITableView to Work With UIPanGestureRecognizer

I have a UIPanGuestureRecognizer added to the entire view using this code:
UIPanGestureRecognizer *pgr = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panAction:)];
[[self view] addGestureRecognizer:pgr];
Within the main view I have a UITableView which has this code to enable the swipe to delete feature:
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"RUNNING2");
return UITableViewCellEditingStyleDelete;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row >= _firstEditableCell && _firstEditableCell != -1)
NSLog(#"RUNNING1");
return YES;
else
return NO;
}
Only RUNNING1 is printed to the log and the Delete button does not show up. I believe the reason for this is the UIPanGestureRecognizer, but I am not sure. If this is correct how should I go about fixing this. If this is not correct please provide the cause and fix. Thanks.
From the document:
If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.
Your UIPanGestureRecognizer recognizes the swipe gesture first, so your UITableView does not receive touches anymore.
To make the table view receives touch simultaneously with the gesture recognizer, add this to the gesture recognizer's delegate:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
If you are using UIPanGuestureRecognizer for example to show side menu you might see some unwanted side effects when you just return YES in all cases as proposed in accepted answer. For example the side menu opening when you scroll up/down the table view (with additional very little left/right direction) or delete button behaving strangely when you open the side menu. What you might want to do to prevent this side effects is to allow only simultaneous horizontal gestures. This will make the delete button work properly but at the same time other unwanted gestures will be blocked when you slide the menu.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)otherGestureRecognizer;
CGPoint velocity = [panGesture velocityInView:panGesture.view];
if (ABS(velocity.x) > ABS(velocity.y))
return YES;
}
return NO;
}
or in Swift:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard let panRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer else {
return false
}
let velocity = panRecognizer.velocity(in: panRecognizer.view)
if (abs(velocity.x) > abs(velocity.y)) {
return true
}
return false
}
If the accepted answer does not work. Try adding
panGestureRecognizer.cancelsTouchesInView = false
Ensure you haven't added the gesture to the tableview directly. I added a pan gesture on the ViewController view and can confirm it works.

Disable gesture recognizer only for a particular view

On one view controller, I have one mainView. On that view I have another view, sidePanel, which has the frame 0,0,86,420. I have added a tap gesture recognizer. Now I want to just enable gesture recognition only for mainView and not for sidePanelView. See below image:
I want to disable tapGesture for sidePanelView and enable for all areas other than it. How can I do that? (One other thing I want to say, area other than sidePanelView is parentView of sidePanelView).
You should accept Bharat's answer because that is correct. I only want to illustrate how you do it:
Define your view controller as conforming to UIGestureRecognizerDelegate, e.g.:
#interface ViewController () <UIGestureRecognizerDelegate>
// the rest of your interface
#end
Make sure you set the delegate for the gesture:
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleMainTap:)];
gesture.delegate = self;
[self.view addGestureRecognizer:gesture];
Then have and then check to see if the touch takes place for the view in question:
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if (CGRectContainsPoint(self.menuView.bounds, [touch locationInView:self.menuView]))
return NO;
return YES;
}
You could use the gestureRecognizer:shouldReceiveTouch: method in your UIGestureRecognizerDelegate to see where the touch occurred and decide whether or not you want to respond to the gesture. Return NO if the touch is too close to the edge of your View(where you want ti disabled), otherwise return YES. Or simply check the touch.view to see if the touch occurred on your UIImageView.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch;
Swift 3 version:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if theView.bounds.contains(touch.location(in: theView)) {
return false
}
return true
}
Ran into a similar issue; ended up using the answer from #Rob. Here's a Swift version:
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
return !CGRectContainsPoint(menuView.bounds, touch.locationInView(menuView))
}
}
If you want to disable UITapGestureRecognizer for a particular view, you just remove userInteraction.
Ex
sidePanel.userInteractionEnabled = NO;
I have did this,with help of
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
and in that i have checked for touch point location & according to touch location i did my work like this
if(points.x>86)
{//hide the side panel
}
It recognizes gestures with synchronize with events.

UIPageViewController Gesture recognizers

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.
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).
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
}
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
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
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
}
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
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.
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;
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.
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 :)
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;
}
}
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.
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)
}
}
}
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.)
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
}

Resources