How can I tell a UIGestureRecognizer to cancel an existing touch? - cocoa-touch

I have a UIPanGestureRecognizer I am using to track an object (UIImageView) below a user's finger. I only care about motion on the X axis, and if the touch strays above or below the object's frame on the Y axis I want to end the touch.
I've got everything I need for determining if a touch is within the object's Y bounds, but I don't know how to cancel the touch event. Flipping the recognizer's cancelsTouchesInView property doesn't seem to do what I want.
Thanks!

This little trick works for me.
#implementation UIGestureRecognizer (Cancel)
- (void)cancel {
self.enabled = NO;
self.enabled = YES;
}
#end
From the UIGestureRecognizer #enabled documentation:
Disables a gesture recognizers so it
does not receive touches. The default
value is YES. If you change this
property to NO while a gesture
recognizer is currently recognizing a
gesture, the gesture recognizer
transitions to a cancelled state.

#matej's answer in Swift.
extension UIGestureRecognizer {
func cancel() {
isEnabled = false
isEnabled = true
}
}

Obj-C:
recognizer.enabled = NO;
recognizer.enabled = YES;
Swift 3:
recognizer.isEnabled = false
recognizer.isEnabled = true

How about this from the apple docs:
#property(nonatomic, getter=isEnabled) BOOL enabled
Disables a gesture recognizers so it does not receive touches. The default value is YES. If you change this property to NO while a gesture recognizer is currently recognizing a gesture, the gesture recognizer transitions to a cancelled state.

According to the documentation you can subclass you gesture recogniser:
In YourPanGestureRecognizer.m:
#import "YourPanGestureRecognizer.h"
#implementation YourPanGestureRecognizer
- (void) cancelGesture {
self.state=UIGestureRecognizerStateCancelled;
}
#end
In YourPanGestureRecognizer.h:
#import <UIKit/UIKit.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface NPPanGestureRecognizer: UIPanGestureRecognizer
- (void) cancelGesture;
#end
Now you can call if from anywhere
YourPanGestureRecognizer *panRecognizer = [[YourPanGestureRecognizer alloc] initWithTarget:self action:#selector(panMoved:)];
[self.view addGestureRecognizer:panRecognizer];
[...]
-(void) panMoved:(YourPanGestureRecognizer*)sender {
[sender cancelGesture]; // This will be called twice
}
Ref: https://developer.apple.com/documentation/uikit/uigesturerecognizer?language=objc

Just set recognizer.state in your handlePan(_ recognizer: UIPanGestureRecognizer) method to .ended or .cancelled

You have a couple ways of handling this:
If you were writing a custom pan gesture recognizer subclass, you could easily do this by calling -ignoreTouch:withEvent: from inside the recognizer when you notice it straying from the area you care about.
Since you're using the standard Pan recognizer, and the touch starts off OK (so you don't want to prevent it with the delegate functions), you really can only make your distinction when you receive the recognizer's target actions. Check the Y value of the translationInView: or locationInView: return values, and clamp it appropriately.

Related

Gesture recogniser does not works on tableView

The tableView was done programatically, and is listen to delegates .
the gestures :
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didTapOnTableView:)];
[self.tableView addGestureRecognizer:tap];
-(void) didTapOnTableView:(UIGestureRecognizer*) recognizer
{
NSLog(#"ffff");
}
does not being called.
The problem is that UITableView has gesture recognizers of its own and they conflict with the ones you're adding. You can allow simultaneous processing of gesture recognizers by conforming your UIViewController subclass to <UIGestureRecognizerDelegate> protocol and implementing the following method gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:.
On the other hand, what you are trying to do is most probably wrong. Try using a UIButton object instead of the gesture recognizer, in 99% of the time that'll be enough.
this should be working, unless you are not tapping on the tableview, check is the tableview showing properly, or add break point to check is the gesture recogniser is added
Try to enable multitouch on the view using interface builder or this property multipleTouchEnabled.
if thats not what you are looking for you can use the delegate function for receiving touches:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return YES;
}
(if you go to the header of this file you can find similar delegates that might help)

UIGestureRecognizer blocking tableview scrolling

I have a table with static cells. One of these cells has a view in it with a pan gesture recogniser on it.
When I am scrolling down my tableview, when I get to the cell with the view with pan gesture recogniser, scrolling doesn't seem to work. If I touch outside the view (to the side or top or bottom) it works and I can scroll. I have an if statement in my gesturerecognizer that tests whether a certain area has been touched, and if so performs an action.
I have looked at this issue (http://stackoverflow.com/questions/3295239/uigesturerecognizer-blocking-table-view-scrolling) but setting cancelsTouchesInView to NO didn't work, I don't have anywhere setting the state property and using the method - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
I don't know where to get the 'otherGestureRecognizer' from or what object to call that method on.
I'm assuming I wan't to put my gesture recogniser as the first argument, and the tableview's scroll gesture recogniser as the otherGestureRecogniser, is that correct? If so, how do I get that?
UIPanGestureRecognizer *windPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(moveWindHandle:)];
[self.windRangeView addGestureRecognizer:windPanGesture];
Then in my moveWindHandle:
-(void)moveWindHandle:(UIPanGestureRecognizer *)gesture
{
gesture.cancelsTouchesInView = NO;
isMovingHandle = [self isPoint:startedTouchAt insideHandle:_toHandleWindImageView];
if(isMovingHandle) {
if(gesture.state == UIGestureRecognizerStateBegan) {
//do stuff
}
}
else
{
//i want it to ignore this gesture and just scroll like normal if that is what hte user did
}
}
I have set the tableviewcontroller as a UIGestureRecognizerDelegate, but I don't know what to do with that.
You would not be the one calling -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:. That method is called by the system. You need to set your table view controller as the delegate for your window pan gesture.
windPanGesture.delegate = self;
At that point, when you do the pan, the system will call the delegate method -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: with your gesture recognizer as one argument and the scroll view's gesture recognizer as the other.
Update
You may also want to implement the -gestureRecognizerShouldBegin: method and return NO if you are not in one of the certain areas.

When does setter method get called from UIView subclass

I am taking the free Stanford course on iTunesU(193P) and we created setting up a class that is a subclass of UIView and created a public property called scale. The idea was that when we pinch, the scale of the view is changed accordingly but I am confused about when the setter of the property scale gets called. here is the relevant code below:
#interface FaceView : UIView
#property (nonatomic) CGFloat scale; //anyone who wants do publicly can set my scale
-(void)pinch:(UIPinchGestureRecognizer *)gesture;
#end
#synthesize scale = _scale;
#define DEFAULT_SCALE 0.90
-(CGFloat)scale{
if(!_scale){
return DEFAULT_SCALE;
}else {
return _scale;
}
}
-(void)setScale:(CGFloat)scale{
NSLog(#"setting the scale");
if(scale != _scale){
_scale = scale;
[self setNeedsDisplay];
}
}
-(void)pinch:(UIPinchGestureRecognizer *)gesture{
if ( (gesture.state == UIGestureRecognizerStateChanged) || (gesture.state == UIGestureRecognizerStateEnded)){
self.scale *= gesture.scale;
gesture.scale = 1;
}
}
When I am in "pinch mode" the setScale method continues to be called as I am pinching as my NSLog statement prints out until I stop the pinch. When or how does the setScale method continued to be called when there isn't any code programmatically calling it? Perhaps I missed something along the way here.
#cspam, remember that to set the gesture recognizer is 2 steps:
1) Adding a gesture recognizer to UIView - This kind of confused me in the lecture but eventhough he is saying add a gesture recognizer to UIView, he really means add a gesture recognizer FOR UIView, IN UIViewController. That is the code that you are missing that you have to add in the UIViewController subclass in this case (the Faceviewcontroller - your name might be different) and that is what will keep calling your pinch method in FaceView above:
UIPinchGestureRecognizer *pinchGesture=[[UIPinchGestureRecognizer alloc]initWithTarget:self.faceView action:#selector(pinch)];
[self.faceView addGestureRecognizer:pinchGesture];
You would add this code in your UIViewController (subclass) in the setter method of your UIView [in other words, create and connect an IBOutlet property in your UIViewController to your UIView in the storyboard] and override the setter method to include the code above.
2) The second part is what you have in your code. So pinch method will be called everytime the controller senses a pinch gesture.
Hope this helps.

Continuous scrolling between UIPanGestureRecognizer and re-enabled UIScrollView

I've got a UIScrollView with paging enabled, and I've added my own UIPanGestureRegonizer to it. Under certain instances, my view controller will set scrollview.scrollEnabled = NO, and then add the pan gesture recognizer to it (I'm not using the scrollview's own recognizer).
So, scrolling is disabled but I'm waiting for user touches from my gesture recognizer. When it recognizes, it calls its action in which I re-enable scrolling.
The problem is, while the user still has a finger down, my scrollview doesn't track with the finger. It doesn't start scrolling until the finger is lifted and then dragged again. So my gesture recognizer is swallowing all the touches and not forwarding any to the scrollview.
I've tried toggling panGestureRecognizer.cancelsTouchesInView = NO; but it doesn't seem to have any effect (I'm currently removing this recognizer as soon as I re-enable scrolling but whether I do this or not doesn't solve my problem). I've also looked into the delays... properties of UIGestureRecognizer but they don't seem to be helping, either.
Any ideas? How can I get these events to continue to forward to my scrollview?
The answer is a bit easier if you are only targeting iOS 5 and up, because in that case you really ought to reuse the UIScrollView panGestureRecognizer property.
In any case, the key step is to NOT reuse scrollEnabled, but instead to subclass UIScrollView, create your own property to manage this state, and override setContentOffset:.
- (void) setContentOffset:(CGPoint)contentOffset
{
if(self.programaticScrollEnabled)
[super setContentOffset:contentOffset];
}
Here's one possible iOS 4+ Solution:
Subclass UIScrollView (or subclass another subclass of UIScrollView, depending on your needs).
Override all the initializers to ensure your setup code is called.
Declare the BOOL property and override setContentOffset: as described above.
In your setup code, set up a UIPanGestureRecognizer and set your state variable to allow programatic scrolling (assuming that's the default state you want):
panRecognizer = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)] autorelease];
//These properties may change according to your needs
panRecognizer.cancelsTouchesInView = NO;
panRecognizer.delaysTouchesBegan = NO;
panRecognizer.delaysTouchesEnded = NO;
[self addGestureRecognizer:panRecognizer];
panRecognizer.delegate = self;
self.programaticScrollEnabled = YES;
Manage which gestures can occur simultaneously. In my case:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Turn programatic scrolling back on wherever you need it. For example:
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer
{
self.programaticScrollEnabled = YES;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
self.programaticScrollEnabled = YES;
return YES;
}

Detect horizontal panning in UITableView

I'm using a UIPanGestureRecognizer to recognize horizontal sliding in a UITableView (on a cell to be precise, though it is added to the table itself). However, this gesture recognizer obviously steals the touches from the table. I already got the pangesturerecognizer to recognize horizontal sliding and then snap to that; but if the user starts by sliding vertical, it should pass all events from that touch to the tableview.
One thing i have tried was disabling the recognizer, but then it wouldn't scroll untill the next touch event. So i'd need it to pass the event right away then.
Another thing i tried was making it scroll myself, but then you will miss the persistent speed after stopping the touch.
Heres some code:
//In the viewdidload method
UIPanGestureRecognizer *slideRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(sliding:)];
[myTable addGestureRecognizer:slideRecognizer];
-(void)sliding:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint translation = [recognizer translationInView:favoritesTable];
if (sqrt(translation.x*translation.x)/sqrt(translation.y*translation.y)>1) {
horizontalScrolling = YES; //BOOL declared in the header file
NSLog(#"horizontal");
//And some code to determine what cell is being scrolled:
CGPoint slideLocation = [recognizer locationInView:myTable];
slidingCell = [myTable indexPathForRowAtPoint:slideLocation];
if (slidingCell.row == 0) {
slidingCell = nil;
}
}
else
{
NSLog(#"cancel");
}
if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled)
{
horizontalScrolling = NO;
}
if (horizontalScrolling)
{
//Perform some code
}
else
{
//Maybe pass the touch from here; It's panning vertically
}
}
So, any advice on how to pass the touches?
Addition: I also thought to maybe subclass the tableview's gesture recognizer method, to first check if it's horizontal; However, then i would need the original code, i suppose... No idea if Apple will have problems with it.
Also: I didn't subclass the UITableView(controller), just the cells. This code is in the viewcontroller which holds the table ;)
I had the same issue and came up with a solution that works with the UIPanGestureRecognizer.
In contrast to Erik I've added the UIPanGestureRecognizer to the cell directly, as I need just one particular cell at once to support the pan. But I guess this should work for Erik's case as well.
Here's the code.
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
UIView *cell = [gestureRecognizer view];
CGPoint translation = [gestureRecognizer translationInView:[cell superview]];
// Check for horizontal gesture
if (fabsf(translation.x) > fabsf(translation.y))
{
return YES;
}
return NO;
}
The calculation for the horizontal gesture is copied form Erik's code – I've tested this with iOS 4.3.
Edit:
I've found out that this implementation prevents the "swipe-to-delete" gesture. To regain that behavior I've added check for the velocity of the gesture to the if-statement above.
if ([gestureRecognizer velocityInView:cell].x < 600 && sqrt(translate...
After playing a bit on my device I came up with a velocity of 500 to 600 which offers in my opinion the best user experience for the transition between the pan and the swipe-to-delete gesture.
My answer is the same as Florian Mielke's, but I've simplified and corrected it some.
How to use:
Simply give your UIPanGestureRecognizer a delegate (UIGestureRecognizerDelegate). For example:
UIPanGestureRecognizer *panner = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panDetected:)];
panner.delegate = self;
[self addGestureRecognizer:panner];
Then have that delegate implement the following method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
CGPoint translation = [(UIPanGestureRecognizer *)gestureRecognizer translationInView:gestureRecognizer.view.superview];
return fabsf(translation.x) > fabsf(translation.y);
}
Maybe you can use the UISwipeGestureRecognizer instead? You can tell it to ignore up/down swipes via the direction property.
You may try using the touch events manually instead of the gesture recognizers. Always passing the event back to the tableview except when you finally recognize the swipe gesture.
Every class that inherits from UIResponder will have the four touch functions (began, ended, canceled, and moved). So the simplest way to "forward" a call is to handle it in your class and then call it explicitly on the next object that you would want to handle it (but you should make sure to check if the object responds to the message first with respondsToSelector: since it is an optional function ). This way, you can detect whatever events you want and also allow the normal touch interaction with whatever other elements need it.
Thanks for the tips! I eventually went for a UITableView subclass, where i check if the movement is horizontal (in which case i use my custom behaviour), and else call [super touchesMoved: withEvent:];.
However, i still don't really get why this works. I checked, and super is a UITableView. It appears i still don't fully understand how this hierarchy works. Can someone try and explain?