I would like to use UIGestureRecognizers to distinguish between two kinds of gestures: those occurring in a mini-UIView and those occurring anywhere else in the view.
I thought I could accomplish this with two GestureRecognizers using requireGestureRecognizerToFail. However, the parent view's GestureRecognizer never fires. Why is this?
Here is a demonstrative test case:
-(void)viewDidLoad {
UIView *miniView = [[[UIView alloc] initWithFrame:CGRectMake(100,100,20,20)] autorelease];
miniView.backgroundColor = [UIColor redColor];
[self.view addSubview:miniView];
// triggered when you tap the miniView
UITapGestureRecognizer *miniTap = [[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(miniTap:)] autorelease];
[miniView addGestureRecognizer:miniTap];
// expected: triggered when you tap anywhere outside the mini-view
// actual: never triggered!
UITapGestureRecognizer *nonMiniTap = [[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(nonMiniTap:)] autorelease];
[nonMiniTap requireGestureRecognizerToFail:miniTap];
[self.view addGestureRecognizer:nonMiniTap];
[super viewDidLoad];
}
-(void)miniTap:(id)sender { NSLog(#"miniTap"); }
-(void)nonMiniTap:(id)sender { NSLog(#"nonMiniTap"); }
Why does requireGestureRecognizerToFail: prevent nonMiniTap from ever being triggered? Can I make it behave in the expected way?
If you see the documentation for requireGestureRecognizerToFail:, it is stated clearly that the gesture's state isn't changed from UIGestureRecognizerStatePossible until the other gesture transitions to UIGestureRecognizerStateFailed or UIGestureRecognizerStateRecognized. But since the miniTap gesture doesn't move out of its default UIGestureRecognizerStatePossible state as the touch isn't within its view, the nonMiniTap gesture isn't recognized. So your taps on the parent view are failing.
As such that dependency isn't required. You can take that line off and it should work as you want it to.
Related
For the past four hours, I have tried many Stack Overlow solutions but none have helped solve my problem.
Here it is,
I have a UIScrollView
Within that ScrollView there is one custom UILabel and 8 custom UIImageViews
I want to detect a long press
Something like this works
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressDidFire:)];
longPress.minimumPressDuration = 0.5;
[scroll addGestureRecognizer:longPress]; //scroll defined elsewhere
However, if I replace the scroll with any subviews of scroll, the long press event never fires.
How do I detect a long press of a subview of a scroll view?
This is quite a messy hack, however, since I can detect long presses of a scroll view, is there any way where I can detect the
position of the press so that I can see which specific subview is
being pressed.
Also, (insert subview here).userInteractionEnabled = YES, I set this property for all my subviews of the scroll view, so this should not be a problem.
I have also tried using touchesBegan and touchesEnded method as suggested elsewhere in Stack Overflow.
Also, for the image views, I do set a new UILongPressGestureRecognizer for every custom image view, using a for loop, as I am aware of the 1 view per gesture recognizer rule.
From A First Time iOS Developer,
Graham
P.S. I'd really prefer if I could find a solution for 1. rather than the messy 2.
More Code As Requested:
In the Init of the view Controller
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressDidFire:)];
longPress.minimumPressDuration = 0.5;
[self.titleLabel addGestureRecognizer:longPress]; //titleLabel property initialized elsewhere
[mainView addSubview:self.titleLabel];
In a "load images" method
for (NSData * dataImg in results) {
//Does some work turning data into an image, into an image view called img
img.delegate = self;
img.userInteractionEnabled = YES;
UILongPressGestureRecognizer *aLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressDidFire:)];
aLongPress.minimumPressDuration = 0.5;
[img addGestureRecognizer:aLongPress];
[imgContainer addSubview:img];
}
Even More Code + Notes
self.view (UIView)
->scroll (UIScrollView)
->->mainView (UIView)
->->->titleLabel (UILabel)
->->->imgContainer (UIView)
->->->->images (UIImageViews)
[self.view addSubview:scroll];
[scroll addSubview:mainView];
[mainView addSubview:self.titleLabel];
[mainView addSubview:imgContainer];
[imgContainer addSubview:img]; //done 8x via for loop
Thanks to #RegularExpression's answer, I now am aware that the mainView is getting pressed, but not its subviews, so I need to find a way to display the mainView's subviews above it. :)
Another update, titleLabel works. ImageViews still don't work though. :(
I know this is a bit late and an answer has been chosen, but in case someone else wants a nice simple solution if you've got iOS7.
Inside your delegate of the UILongPressGestureRecognizer implement the gestureRecognizer:shouldRequireFailureOfGestureRecognizer:otherGestureRecognizer selector
Check if otherGestureRecognizer is a UIPanGestureRecognizer and return YES, otherwise return NO
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
return YES;
}
return NO;
}
The scroll view will actually produce a UIScrollViewPanGestureRecognizer, which is part of the private API, but it's a subclass of UIPanGestureRecognizer so the above works fine.
To support iOS6 or below, then you'll need to loop through the gestureRecognizers of the UIScrollView, detect which one is a UIPanGestureRecognizer and perform the requireGestureRecognizerToFail selector on your UILongPressGestureRecogizer with that.
your code seems to be fine,it should work i think.i used below code and its working fine for me.
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPress:)];
longPress.delegate = (id)self;
longPress.minimumPressDuration=0.05;
imageView.userInteractionEnabled = YES;
[imageView addGestureRecognizer:longPress];
and its method,
- (IBAction)handleLongPress:(UILongPressGestureRecognizer *)sender {
NSLog(#"detected");
if (sender.state == UIGestureRecognizerStateEnded){
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Alert" message:#"YES" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles: nil];
[alert show];
}
}
Here i took imageView as subview of scrollview as u said
Since your UIScrollView is the common parent, that's probably where your gesture recognizer needs to be. You can determine which subview is being pressed by looking at the location of the point supplied in your action. So, the individual subviews do not need gesture recognizers.
So, you would do something like this:
- (void)longPressDidFire:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
CGPoint point = [sender locationInView:scroll];
UIView *tappedView = [scroll hitTest:point withEvent:nil];
So, then you have the view that was long-pressed.
Other things that could cause the action not to fire would be a delegate problem or if the scroll is contained by another view that is intercepting the touch.
HTH
Instead of
[scroll addGestureRecognizer:longPress];
Add the gesture on your subviews, right after you declare them and before you add them to scrollview
[subview addGestureRecognizer:longPress];
Woohoo it works!
The problem was:
imgContainer was a UIView with an empty frame with several UIImageViews as subviews
I was under the impression that as I added a subview to imgContainer, imgContainer would expand.
This is not true.
I had to set imgContainer's frame to the same content frame as the scroll view and then everything became ok.
I hope this answer helps any other future iOS firs timers like me.
I have a view controller where I display a grid/array of images, where every image view is a custom nib (custom nib because images have a name & like/dislike icon too). So I displayed the grid of images doing something like this in my view controller viewDidLoad.
int row=0, col=0;
for (int i=0; i<arrayImg.count; i++) {
NSArray *topObj = [[NSBundle mainBundle] loadNibNamed:#"CustomImageView" owner:nil options:nil];
CustomImageView *imgView = [topObj objectAtIndex:0];
imgView.frame = CGRectMake(180*col+10, 180*row+10, 170, 170);
// custom image values inserted here
[self.view addSubView:imgView];
// update the row,col variables here
}
Now I need to add a tap gesture recognizer to every image displayed on the screen. It seems logical to me to add the gesture recognizer inside the custom nib/class, CustomImageView in this case. CustomImageView extends UIView, so it seems a gesture recognizer cannot be declared here (auto-complete does not appear, syntax highlighting does not work either). What am I missing over here?
You can surely add a gesture recognizer to your CustomImageView (provided it is a UIView). Try something like this:
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
[tapRecognizer setNumberOfTapsRequired:1];
[tapRecognizer setDelegate:self];
[imgView addGestureRecognizer:tapRecognizer];
Note that the only method that you should see auto-completed is addGestureRecognizer.
In general, prefer the official documentation (or the compiler, if you like) over auto-completion in order to decide whether a feature is present or not. Auto-completion is not always right, in my experience.
Previously, I asked about how to capture any touch input on an MPMoviePlayerController's view when the MPMovieControlStyle is set to MPMovieControlStyleNone. It was suggested that I could use a UIGestureRecognizer to do this.
I am able to capture double taps on the screen using a gesture recognizer in this manner, but not single taps. The code I used for this is as follows:
///**********///
singleTapGestureRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleClickOnMediaView:)];
singleTapGestureRecognizer.numberOfTapsRequired = 1;
[self.moviePlayer.view addGestureRecognizer:singleTapGestureRecognizer];
[singleTapGestureRecognizer release];
///**********///
Why am I unable to capture single taps on an MPMoviePlayerController's view using this code? Is there something special about how it handles single taps?
I know it's a bit old question, but here's a solution, if anyone needs this.
To handle single and double taps on the same view, the single tap recognizer has to wait for the double tap recognizer to fail. Something like this:
UITapGestureRecognizer* doubleTapRecon = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleDoubleTap)];
[doubleTapRecon setNumberOfTapsRequired:2];
[doubleTapRecon setDelegate:self];
[self.view addGestureRecognizer:doubleTapRecon];
UITapGestureRecognizer* singleTapRecon = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap)];
[singleTapRecon setNumberOfTapsRequired:1];
[singleTapRecon requireGestureRecognizerToFail:doubleTapRecon];
[singleTapRecon setDelegate:self];
[self.view addGestureRecognizer:singleTapRecon];
Note, that if you are not using ARC, you have to take care of memory managment.
No the above answer will not work to get single tap gesture you have to implement 'UITapGestureRecognizerDelegate' and use the method
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Because 'moviePlayer' is also using tap gesture, so to get working our custom single Tap Only above method is required.
I have a parent view called ImageViewController. To this view I add up to two subviews called ImageDetailViewController. In this detail view controller I declare a few gestures that need to do certain things on their respective views only. Here is the code:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(changeModeButtonPushed:)];
[tap setNumberOfTapsRequired:2];
[tap setNumberOfTouchesRequired:1];
[tap setDelegate:self];
[openGLView addGestureRecognizer:tap];
[tap release];
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(touchDidDrag:)];
[panRecognizer setMaximumNumberOfTouches:1];
[openGLView addGestureRecognizer:panRecognizer];
[panRecognizer release];
UIPanGestureRecognizer *shiftRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(shiftView:)];
[shiftRecognizer setMinimumNumberOfTouches:2];
[openGLView addGestureRecognizer:shiftRecognizer];
[shiftRecognizer release];
UIPinchGestureRecognizer *pinchRecog = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(viewPinched:)];
[openGLView addGestureRecognizer:pinchRecog];
[pinchRecog release];
So I need to have two of these detail views load up next to each other inside of this parent view, and then I need to be able to do these gestures on one view or the other, and have that view respond.
The problem is that when I 1 finger pan around with 1 finger, sometimes it will call on the view it is supposed to, but most often, it fires on the other view! It doesn't make any sense! I have tried debugging and everything else I can think of, but I just can't see how two separately declared objects can get mixed up like this.
To make things stranger, there are some gestures that do work correctly. So it's only this one finger pan around gesture that screws everything up.
What could I be missing? How can I even debug this? Everything is looking like the objects are indeed separate, but they just aren't behaving that way...
You could debug/solve this by finding out what view your gesture recogniser is calling on: Have a look at this code:
(void)NAMEOFFUNCTION:(UITapGestureRecognizer*)recognizer {
if ([yourViewController.view pointInside:[recognizer locationInView:presentationSlideViewController.view] withEvent:nil]) {
}
filter through all your subviews, in this example i'm checking to see if the finger touch is below a defined x and then y coordinate. First check to see if the point is inside the _buttonVIew subview://
if ([_buttonView pointInside:[recognizer locationInView:_buttonView] withEvent:nil]) {
Then check to see if the touch is in a coordinate lower than 50
if (([recognizer locationInView:_buttonView].x < 50) && ([_leftArrow isEnabled])) {
pageIndex -= 1;
}
//otheriwse check to see if its over 970
else if (([recognizer locationInView:_buttonView].x > 970) && ([_rightArrow isEnabled])) {
pageIndex += 1;
}
[self scrollViewScrollToRect:pageIndex];
[self fadeArrows:1.0];
return;
}
}
I have a UIView inside of a UIScrollView, both created using IB. The UIView scrolls horizontally inside the UIScrollView. I want to detect left and right 2 finger swipes.
Borrowing from the sample code I found in SmpleGestureRecognizers, I have put the following code in the viewDidLoad method of the UIScrollView's ViewController...
UIGestureRecognizer *recognizer;
UISwipeGestureRecognizer *swipeRightRecognizer, *swipeLeftRecognizer;
recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipeFrom:)];
swipeRightRecognizer = (UISwipeGestureRecognizer *)recognizer;
swipeRightRecognizer.numberOfTouchesRequired = 2;
[self.view addGestureRecognizer:swipeRightRecognizer];
[recognizer release];
recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipeFrom:)];
swipeLeftRecognizer = (UISwipeGestureRecognizer *)recognizer;
swipeLeftRecognizer.numberOfTouchesRequired = 2;
swipeLeftRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
[self.view addGestureRecognizer:swipeLeftRecognizer];
[recognizer release];
I have set in the viewcontroller.h. and have the following delegate method...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return YES;
}
I am assuming this is a valid gestureRecognizer delegate method, but I cannot find any reference to it in the documentation.
I do not get any errors but nothing happens when I do a 2 finger swipe. The delegate method is not called and neither is my action method. I tried removing the numbeOfTouchesRequired call to see if it might work with a single finger swipe to no avail.
Am I adding the gestureRecognizers to the right view? I tried adding it to the UIView, the UIScrollView as well as self.view.superView.
The sample code runs great. The only difference I can see between my implementation of the code and the sample code is the fact that I used IB to create the views and the sample code did not. I suspect that something is consuming the swipe gesture before it gets to my recognizers.
What am I doing wrong.
Thanks,
John
I had the same problem and I solved by using UIPanGestureRecognizer instead of UISwipeGestureRecognizer.
To emulate the detection of swipe, we'll play with the speed of gesture in scrollview.
If the speed of x direction >= 3000 (for example) the swipe will be detected.
If x>0 it will be a right swipe.
The code I implemented to resolve your situation is:
In a uiscrollview named _scroll1:
UIPanGestureRecognizer *pan;
pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(Swipe4ScrollViews:)];
[pan setMinimumNumberOfTouches:2];
[_scroll1 addGestureRecognizer:pan];
[pan release];
With a global BOOL variable named _panning, Swipe4ScrollViews will do the hard job:
-(void)Swipe4ScrollViews:(UIPanGestureRecognizer *)sender
{
if(sender.state == UIGestureRecognizerStateBegan) _panning = NO;
CGPoint v =[sender velocityInView:_scroll1];
NSLog(#"%f, %f",v.x,v.y);
if( (abs(v.x) >= UMBRAL) && !_panning)
{
_panning = YES;
[sender cancelsTouchesInView];
if(v.x>0) NSLog(#"Right");
else NSLog(#"Left");
[self doSomething];
}
}
I encapsulated it on a UIGestureRecognizer subclass: UISwipe4ScrollGestureRecognizer
The biggest difference between the sample code and your code is that your code involves a UIScrollView.
Internally, scroll views, table views, and web views all use gesture recognizers to some degree. If you're expecting to receive gestures within those views – gestures that are similar to the ones already supported internally – they will almost certainly be consumed or significantly delayed before you can get to them. Receiving gestures outside or above those views should work fine, if your use case supports it.
Try setting the delayContentTouches-property of the UIScrollView to NO, maybe it'll help.