UIButton interaction inside UIPageViewController - iphone

I'm using an UIPageViewController in my application and I wanted to have a few UIButtons inside it, sort of like a menu. The problem I have is that when I put an UIButton (or any other interactive element) near the edges of the screen and tap it, instead of the UIButton action being applied, what happens is that the page changes (because the tap on the edge of the screen changes the page on the UIPageViewController). I'd like to know if there's a way to make it so that the UIButton has higher priority than the UIPageViewController so that when I tap the button, it applies the appropriate action instead of changing the page.

I came here with the same problem. Split’s link has the answer.
Make your root view controller the delegate of each of the UIPageViewController’s gesture recognizers, then prevent touches from being delivered if they occur inside any UIControl:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return ([touch.view isKindOfClass:[UIControl class]] == NO);
}

UIPageViewController has two UIGestureRecognizers. You can access them via gestureRecognizers property. Determine which one is UITapGestureRecognizer and then use this. Hope this helps.

For people that just want to copy/paste code, here is mine :
// I don't want the tap on borders to change the page
-(void) desactivatePageChangerGesture {
for (UIGestureRecognizer* gestureRecognizer in self.pageViewController.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
gestureRecognizer.enabled = NO;
}
}
}
Just call this function after the UIPageViewController creation.

I had this same problem, and was unsure how to handle the UIGestureRecognizer delegate methods. This short example assumes you are using the "Page Based Application" project type in Xcode 4. Here is what I did:
In RootViewController.h, I made sure to announce that RootViewController would handle the UIGestureRecognizerDelegate protocol:
#interface RootViewController : UIViewController <UIPageViewControllerDelegate, UIGestureRecognizerDelegate>
In RootViewController.m, I assigned RootViewController as the delegate for the UITapGestureRecognizer. This is done at the end of the viewDidLoad method. I did this by iterating over each gestureRecognizer to see which one was the UITapGestureRecognizer.
NSEnumerator *gestureLoop = [self.view.gestureRecognizers objectEnumerator];
id gestureRecognizer;
while (gestureRecognizer = [gestureLoop nextObject]) {
if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
[(UITapGestureRecognizer *)gestureRecognizer setDelegate:self];
}
}
Finally, I added the gestureRecognizer:shouldReceiveTouch method to the bottom of RootViewController.m (This is copied directly from Split's link):
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if ([touch.view isKindOfClass:[UIControl class]]) {
// we touched a button, slider, or other UIControl
return NO; // ignore the touch
}
return YES; // handle the touch
}

Comment out these line from your code
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;
or use UIGestureRecognizer as told by Split
Hope this will help you

OLD ANSWER: If your UIPageViewController has a transitionStyle of UIPageViewControllerTransitionStyleScroll and you are in iOS 6.0+, then you can't use the gestureRecognizer:shouldReceiveTouch: method, because there is no way to set the delegate to self on the gestureRecognizers since pageViewController.gestureRecognizers will return nil. See UIPageViewController returns no Gesture Recognizers in iOS 6 for more information about that.
If you simply want to make sure your UIPageViewController passes along button touch events to a UIButton, you can use
for (UIScrollView *view in _pageViewController.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
view.delaysContentTouches = NO;
}
}
if you have a transitionStyle of UIPageViewControllerTransitionStyleScroll and you are in iOS 6.0+.
See this answer about why delaysContentTouches = NO is needed for some cases of a UIButton in a UIScrollView
UPDATE: After doing a little more research it appears that if your issue is that the UIButton click seems to only be called sometimes, then that is actually probably the desired behavior inside a UIScrollView. A UIScrollView uses the delaysContentTouches property to automatically determine if the user was trying to scroll or trying to press a button inside the scroll view. I would assume it is best to not alter this behavior to default to NO since doing so will result in an inability to scroll if the user's finger is over a button.

None of the solutions here where you intercept the UIPageViewController's tap gesture recognizers worked for me. I'm targeting iOS 8 and 9.
What worked is to override the functions touchesBegan, touchesCancelled, touchesMoved, and touchesEnded in my custom button which is a subclass of UIControl. Then I just manually send the .TouchUpInside control event if the touch began and ended within the frame of my custom button.
I didn't have to do anything special for the containing page view controller, or the view controller that contains the page view controller.

Swift 5 answer here should do the job.
pageViewController.view.subviews.compactMap({ $0 as? UIScrollView }).first?.delaysContentTouches = false

Related

Pass taps through a UIPanGestureRecognizer

I'd like to detect swipe on the entire screen, however, the screen contains UIButtons, and if the user taps one of these buttons, I want the Touch Up Inside event to be triggered.
I've create a UIView on the top of my screen, and added a UIPanGestureRecognizer on it to detect the swipe, but now I need to pass the gesture through that view when I detect that it's a tap rather than a swipe.
I know how to differentiate the gestures, but I've no idea on how to pass it to the view below.
Can anyone help on that? Thanks!
Thanks for your answer. The link helped me to solve part of my problem.
I've set the buttons as subviews of my gestureRecognizer view and I can now start a swipe from one of the buttons (and continue to use the buttons as well). I managed to prevent the buttons to go to the "down" state by using the following code :
UIPanGestureRecognizer *swipe = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(swipeDetected:)];
swipe.maximumNumberOfTouches = 1;
swipe.delaysTouchesBegan =YES;
swipe.cancelsTouchesInView = YES;
[self.gestureRecognitionView addGestureRecognizer:swipe];
there is a BOOL property of UIGestureRecognizer cancelsTouchesInView. default is yes. set it to NO , and the touches will pass thru to the UIView
also have a look at the solution for this question
If you want to prevent the recognizer from receiving the touch at all, UIGestureRecognizerDelegate has a method gestureRecognizer:shouldReceiveTouch: you can use:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// don't override any other recognizers
if (gestureRecognizer != panRecognizer) {
return YES;
}
CGPoint touchedPoint = [touch locationInView:self.someButton];
return CGRectContainsPoint(self.someButton.bounds, touchedPoint) == NO;
}

Disable receiving touches from parent view on subview

I have a UIView parentView which implements a UITapGuestureRecognizer and does something when tapped. parentView has a sub view called childView which also implements a UITapGuestureRecognizer and does something when tapped.
There is an instance when I have to turn off the childViews UITapGestureRecognizer during an animation for a slight moment, and I noticed when it is turned off and I tap childView, the tap gets intercepted by parentView. Also, I have a toolbar attached to the top of this view that doesn't have any gesture recognizer attached to it, and it's touches get passed parentView (the buttons will barely work). I'm wondering is it possible to disable this without having a reference to the parents UITapGestureRecognizer?
I've tried using the exclusiveTouches property of UIView set to yes and it doesn't work. Any suggestions would be appreciated.
In the parent gesture recognizers, implement the UIGestureRecognizerDelegate, and implement the following method:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([touch.view isKindOfClass:[ClassThatYouWantTouchesBlocked class]])
{
return NO;
}
else
{
return YES;
}
}
Replace ClassThatYouWantTouchesBlocked with the class that you want its touches to be ignored.
What if never turn off the ChildView's tap gesture recognizer? if it is animating return from the child's tap gesture method without doing anything.
If it is an imageView than isAnimating property might come in use.
Add gesture recognizer to blocker child.
self.blockerView.addGestureRecognizer(UITapGestureRecognizer())

UIGestureRecognizer with both setDelegate and action

I'm trying to add a UIPanGestureController to my UITableView so I can detect whether the user is manually panning or they just gave an initial kick and watch the view scroll by itself. The reason is that I want to snap to a cell as soon as the scrolling slows down (imagine a wheel of fortune). Of course I don't want to snap when the user is panning manually.
However, I can either use the gesture controller (and set my "is scrolling manually" variables accordingly") OR scroll the view.
Using TouchBegin events instead of the gesture recognizer introduces new problems, so that's not really an option.
First I changed the table view to include the UIGestureRecognizerDelegate.
After initializing the view, I then do...
panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panTableView:)];
[panGestureRecognizer setDelegate:self];
[self.view addGestureRecognizer:panGestureRecognizer];
I implement the Begin function, to set a BOOL:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
isPanning = YES;
return NO;
}
Always returning NO should make sure that the gesture recognizer is never active, since I want (and need) to use the table view's own scroll methods.
Problem: my action "panTableView" is never called.
If I don't set the delegate, the action is called, but I can't scroll, since the gesture recognizer catches all my touches.
I already looked into the targets. After setting the delegate, the gesture recognizer's view and delegate pointers are the same as self.view, the action still targets self with the right selector.
try
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
isPanning = YES;
return YES;
}
and implement this
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES
}

hitTest:withEvent: Not Working

I'm making an app where I have a background view and that has six UIImageView's as subviews. I have a UITapGestureRecognizer to see when one of the UIImageViews is tapped on and thie handleTap method below is what the gesture recognizer calls. However, when I run this, the hitTest:withEvent: always returns the background view even when I tap on one of the imageViews. Does it have something to do with the event when I call hitTest?
Thanks
- (void) handleTap: (UITapGestureRecognizer *) sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView: sender.view];
UIView * viewHit = [sender.view hitTest:location withEvent:NULL];
NSLog(#"%#", [viewHit class]);
if (viewHit == sender.view) {}
else if ([viewHit isKindOfClass:[UIImageView class]])
{
[self imageViewTapped: viewHit];
NSLog(#"ImageViewTapped!");
}
}
}
UIImageView are, by default, configured to not register user interaction.
From the UIImageView documentation:
New image view objects are configured to disregard user events by
default. If you want to handle events in a custom subclass of
UIImageView, you must explicitly change the value of the
userInteractionEnabled property to YES after initializing the object.
So, right after you initialize your views you should have:
view.userInteractionEnabled = YES;
This will turn the interaction back on and you should be able to register touch events.
There's a rewrite on your approach (single GR on the containing view) that works, but it'll make our brain hurt getting the coordinate systems right, which is definitely the problem in the posted code.
The better answer is to attach N gesture recognizers to each of the UIImageViews. They can all have the same target and use the same handleTap: method. The handleTap: can get the view without searching any geometry like this:
UIImageView *viewHit = (UIImageView *)sender.view;

Dragging UIButtons in UIScrollView?

I have a scrollview in my Main view and I have three subviews on my scrollview. And I have UIButtons in all my subviews.
Now, I want to drag those buttons from one subview to another subview (while dragging the buttons, the scrollview should not scroll).
How do I do this?
I'm not completely sure if this snippet works for this particular case (an UIControl inside a UIScrollView), but my understanding of UIResponder chain suggests me that it should :)
- (void)viewDidLoad { // or wherever you initialize your things
...
// Add swipe event to UIButton so it will capture swipe intents
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] init];
[panGR addTarget:self action:#selector(panEvent:)];
[button addGestureRecognizer:panGR];
[panGR release];
}
- (void)panEvent:(id)sender {
button.center = [sender locationInView:self.view];
}
If this works (can't test it right now, but it did work for me in a similar situation), then you should add more code to handle the drag & drop related events (maybe disable Clip Subviews option in the UIScrollView, add the button to the new superview if the location intersects with the CGRect of the destination, return the button to the original location if it doesn't, etc).
So, what's happening in those lines? When you begin touching the UIButton, the order doesn't get to the UIScrollView because the event could follow as a touch event (handled by the UIButton), or as a pan event (handled by the UIScrollView). When you move your finger, the event is dismissed by the UIButton's responder because there's no Gesture Recognizer that knows how to proceed if the finger is moved.
But when you add a Gesture Recognizer to the UIButton who actually knows what to do when the finger is moved, everything is different: the UIButton will not dismiss the event, and the UIScrollView will never realize that there was a touch moving over it.
I hope my explanation is accurate and comprensible enough. Let me know if a) it doesn't work or b) there's something unclear.
Good luck :)
Try
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if (allowAppDrag && [gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
return NO;
}
return YES;
}