I'm trying to track hits on UI elements (tap and long press) using UIGestureRecognizer. After hit was tracked (let's say logged via NSLog) UI element should do it's job.
I'm creating gesture recognizers like this:
UITapGestureRecognizer* tap = [[UITapGestureRecognizer] alloc initWithTarget:self action:(OnGesture:)]
tap.cancelsTouchesInView = NO;
tap.delegate = self;
[view addGestureRecognizer:tap];
UILongPressGestureRecognizer* longPress = [[UILongPressGestureRecognizer] alloc initWithTarget:self action:(OnGesture:)]
longPress.cancelsTouchesInView = NO;
longPress.delegate = self;
[view addGestureRecognizer:longPress];
I've overridden some gesture recognizer methods:
-(BOOL)gestureRecognizer:(UIGestureRecognizer*)_recognizer shouldReceiveTouch(UITouch*)_touch
{
return YES;
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer*)_recognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)_otherRecognizer
{
return YES;
}
Inside the gesture recognizer handler, I'm trying to find the exact subview of the tap by using the hitTest method.
-(void)OnGesture:(UIGestureRecognizer*)_recognizer
{
if([_recognizer.state == UIGestureRecognizerStateEnded])
{
if([_recognizer isKindOfClass:[UITapGestureRecognizer class]]
|| [_recognizer isKindOfClass:[UILongPressGestureRecognizer class])
{
CGPoint location = [_recognizer locationOfTouch:0 inView:_recognizer.view];
// my problem occurs here:
//---------------------------------------------------------------------------
UIView* hitView = [_recognizer.view hitTest:location withEvent:nil];
//---------------------------------------------------------------------------
NSLog(#"Hit on view: %#", hitView);
}
}
}
So my problem is:
Sometimes (1 out of 10 cases) when I press the UIButton OnGesture method fires, but the IBAction of the "Touch Up Inside" event of that button is not firing.
But when I comment out hitTest call:
//UIView* hitView = [_recognizer.view hitTest:location withEvent:nil];
the bug stops being reproducible. IBAction always gets called.
Why is this happening? How can I fix this?
P.S. there could be some typos in the sample code above.
According to the docs, in order for it to work:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.
So you might wanna do self.someSubview.userInteractionEnabled = YES;
I have a custom subclass of MKPinAnnotationView that displays a custom call out. I want to handle touch events inside that annotation.
I have a working solution (below) but it just doesn't feel right. I have a rule of thumb that whenever I use performSelector: .. withDelay: I'm fighting the system rather than working with it.
Does anyone have a good, clean workaround for the aggressive event handling of MKMapView and annotation selection handling?
My current solution:
(All code from my annotation selection class)
I do my own hit testing (without this my gesture recognisers don't fire as the Map View consumes the events:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event; {
// To enable our gesture recogniser to fire. we have to hit test and return the correct view for events inside the callout.
UIView* hitView = nil;
if (self.selected) {
// check if we tpped inside the custom view
if (CGRectContainsPoint(self.customView.frame, point))
hitView = self.customView;
}
if(hitView) {
// If we are performing a gesture recogniser (and hence want to consume the action)
// we need to turn off selections for the annotation temporarily
// the are re-enabled in the gesture recogniser.
self.selectionEnabled = NO;
// *1* The re-enable selection a moment later
[self performSelector:#selector(enableAnnotationSelection) withObject:nil afterDelay:kAnnotationSelectionDelay];
} else {
// We didn't hit test so pass up the chain.
hitView = [super hitTest:point withEvent:event];
}
return hitView;
}
Note that I also turn off selections so that in my overridden setSelected I can ignore the deselection.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated; {
// If we have hit tested positive for one of our views with a gesture recogniser, temporarily
// disable selections through _selectionEnabled
if(!_selectionEnabled){
// Note that from here one, we are out of sync with the enclosing map view
// we're just displaying out callout even though it thinks we've been deselected
return;
}
if(selected) {
// deleted code to set up view here
[self addSubview:_customView];
_mainTapGestureRecogniser = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(calloutTapped:)];
[_customView addGestureRecognizer: _mainTapGestureRecogniser];
} else {
self.selected = NO;
[_customView removeFromSuperview];
}
}
It's the line commented 1 that I don't like but it's also pretty hairy at the end of theta delayed fire. I have to walk back up the superview chain to get to the mapView so that I can convince it the selection is still in place.
// Locate the mapview so that we can ensure it has the correct annotation selection state after we have ignored selections.
- (void)enableAnnotationSelection {
// This reenables the seelction state and resets the parent map view's idea of the
// correct selection i.e. us.
MKMapView* map = [self findMapView];
[map selectAnnotation:self.annotation animated:NO];
_selectionEnabled = YES;
}
with
-(MKMapView*)findMapView; {
UIView* view = [self superview];
while(!_mapView) {
if([view isKindOfClass:[MKMapView class]]) {
_mapView = (MKMapView*)view;
} else if ([view isKindOfClass:[UIWindow class]]){
return nil;
} else{
view = [view superview];
if(!view)
return nil;
}
}
return _mapView;
}
This all seems to work without and downside (like flicker I've seen from other solutions. It's relatively straightforward but it doesn't feel right.
Anyone have a better solution?
I don't think you need to monkey with the map view's selection tracking. If I'm correctly interpreting what you want, you should be able to accomplish it with just the hitTest:withEvent: override and canShowCallout set to NO. In setSelected: perform your callout appearance/disappearance animation accordingly. You should also override setHighlighted: and adjust the display of your custom callout if visible.
I am trying to handle tableViewCell's being tapped, but the problem is that this is a "temporary tableView". I have it coded so that it will appear while the user is editing a UITextField, but then I set up a gesture recognizer to set the tableview to hidden as soon as the user clicks somewhere away from the UITextField.
I have the gesture recognizer set up as follows:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:#selector(dismissKeyboard)];
[tap setCancelsTouchesInView:NO];
[self.view addGestureRecognizer:tap];
However, dismissKeyboard is called before didSelectRowAtIndexPath is called, and so the TableView that I want to handle the event on becomes hidden and therefore this function is never called.
My question is: Does anybody have ideas of how to get around this, so that didSelectRowAtIndexPath will execute before the tableView hides? I had one idea to somehow see if the tableView is where the tap is coming from, and if so, then don't execute the "hide tableView" line within dismissKeyboard. Is this possible?
Sorry, but I am new to iOS dev, so thank you for any advice!
You should be able to do this by making your view controller the tap gesture's delegate and denying it any touches that are inside the table view. Here is a starting point:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gesture shouldReceiveTouch:(UITouch *)touch
{
//Assuming your table view is a direct subview of the gesture recognizer's view
BOOL isInsideTableView = CGRectContainsPoint(tableView.frame, [touch locationInView:gesture.view])
if (isInsideTableView)
return NO;
return YES;
}
Hope this helps!
You could set yourself as a delegate to the UITapGestureRecognizer and cancel the gesture when the user taps within the tableView.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//You can also (and should) check to make sure the gestureRecognizer is the tapGestureRecognizer
if (touch.view == tableView)
{
return NO;
}
else
{
return YES;
}
}
To better fit what you need, judge if your search bar is first responder.
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gesture shouldReceiveTouch:(UITouch *)touch
{
BOOL isInsideTableView = CGRectContainsPoint(yourTabelView.frame, [touch locationInView:gesture.view]);
if (isInsideTableView && ![yourSearchBar isFirstResponder])
return NO;
return YES;
}
I have a UITapGestureRecognizer that will hide and show a toolbar over my MKMap when the user taps the Map - simple.
However, when the user taps on an MKMapAnnotation, I do not want the map to respond to a tap in the normal way (above). Additionally, when the user taps elsewhere on the map to de-select an MKAnnotation callout, I also don't want the toolbar to respond. So, the toolbar should only respond when there are no MKAnnotations currently in selected state. Nor should it respond when the user clicks on an annotation directly.
So far, I have being trying the following action that reacts to the tap gesture on the map - however the Annotation View is never detected (the first if statement) and also, the annotation view is also launched regardless of this method.
-(void)mapViewTapped:(UITapGestureRecognizer *)tgr
{
CGPoint p = [tgr locationInView:self.mapView];
UIView *v = [self.mapView hitTest:p withEvent:nil];
id<MKAnnotation> ann = nil;
if ([v isKindOfClass:[MKAnnotationView class]])<---- THIS CONDITION IS NEVER MET BUT ANNOTATIONS ARE SELECTED ANYWAY
{
//annotation view was tapped, select it…
ann = ((AircraftAnnotationView *)v).annotation;
[self.mapView selectAnnotation:ann animated:YES];
}
else
{
//annotation view was not tapped, deselect if some ann is selected...
if (self.mapView.selectedAnnotations.count != 0)
{
ann = [self.mapView.selectedAnnotations objectAtIndex:0];
[self.mapView deselectAnnotation:ann animated:YES];
}
// If no annotation view is selected currently then assume control of
// the navigation bar.
else{
[self showToolBar:self.navigationController.toolbar.hidden];
}
}
}
I need to control the launch of the annotation call out programmatically and detect when the tap event has hit an annotation in order to achieve this.
Any help would be appreciated.
I think you will find the following links very useful:
http://blog.asynchrony.com/2010/09/building-custom-map-annotation-callouts-part-2/
How do I make a MKAnnotationView touch sensitive?
The first link discusses (among other things) how to prevent the propagation of touches to the annotations so that they selectively respond, and the second one how to detect the touches.
I think that because MKMapAnnotationView are on top of MKMapView, they will get the touch event and respond to it (be selected) so I don't think you need to select your annotation manually.
Then, if you have a look at Advanced Gesture Recognizer WWDC 2010 video, you will see that your MKMapView will receive tap event anyway, even if it's below the annotation view. That's probably why your -(void)mapViewTapped:(UITapGestureRecognizer *)tgr method get called.
Apart from that, I can't see why your if ([v isKindOfClass:[MKAnnotationView class]]) is never true. I do the exact same thing in my code and it works fine!
Finally, to answer your last question, if you don't want to do anything when the user is just trying to close the callout, you could keep track of a custom isCalloutOpen boolean value like this:
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
//some code
_isCalloutOpen = YES;
}
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view {
// delay the reset because didDeselectAnnotationView could (and is often) called before your gesture recgnizer handler method get called.
[self performSelector:#selector(resetCalloutOpenState) withObject:Nil afterDelay:0.1];
}
- (void)resetCalloutOpenState {
_isCalloutOpen = NO;
}
- (void)mapViewTapped:(UITapGestureRecognizer *)tgr {
if (_isCalloutOpen) {
return;
}
}
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
}