I am struggling to get the behaviour I would like from the gesture recognisers, specifically cancelling certain gestures if others have fired.
I have a scrollView set to paging and multiple subviews in each page. I have added a touch gesture recogniser to scroll to the next or prev page if the user taps to the right or left of the page.
// Add a gesture recogniser turn pages on a single tap at the edge of a page
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapGestureHandler:)];
tapGesture.cancelsTouchesInView = NO;
[self addGestureRecognizer:tapGesture];
[tapGesture release];
and my gesture handler:
- (void) tapGestureHandler:(UIGestureRecognizer *) gestureRecognizer {
const CGFloat kTapMargin = 180;
// Get the position of the point tapped in the window co-ordinate system
CGPoint tapPoint = [gestureRecognizer locationInView:nil];
// If the tap point is to the left of the page then go back a page
if (tapPoint.x > (self.frame.size.width - kTapMargin)) [self scrollRectToVisible:pageViewRightFrame animated:YES];
// If the tap point is to the right of the page then go forward a page
else if (tapPoint.x < kTapMargin) [self scrollRectToVisible:pageViewLeftFrame animated:YES];
}
All works well, except where I have a subview on the page that has buttons in it. I want to be able to ignore the tap to turn the page if the user touches a button on the subView and I can't figure out how to do this.
Cheers
Dave
The solution that worked the best for me in the end was to use the hitTest to determine if there were any buttons underneath the location of the tap gesture. If there are then just ignore the rest of the gesture code.
Seems to work well. Would like to know if there are any gotchas with what I have done.
- (void) tapGestureHandler:(UIGestureRecognizer *) gestureRecognizer {
const CGFloat kTapMargin = 180;
// Get the position of the point tapped in the window co-ordinate system
CGPoint tapPoint = [gestureRecognizer locationInView:nil];
// If there are no buttons beneath this tap then move to the next page if near the page edge
UIView *viewAtBottomOfHeirachy = [self.window hitTest:tapPoint withEvent:nil];
if (![viewAtBottomOfHeirachy isKindOfClass:[UIButton class]]) {
// If the tap point is to the left of the page then go back a page
if (tapPoint.x > (self.bounds.size.width - kTapMargin)) [self scrollRectToVisible:pageViewRightFrame animated:YES];
// If the tap point is to the right of the page then go forward a page
else if (tapPoint.x < kTapMargin) [self scrollRectToVisible:pageViewLeftFrame animated:YES];
}
}
Apple documentation shows the answer:
- (void)viewDidLoad {
[super viewDidLoad];
// Add the delegate to the tap gesture recognizer
self.tapGestureRecognizer.delegate = self;
}
// Implement the UIGestureRecognizerDelegate method
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch: (UITouch *)touch {
// Determine if the touch is inside the custom subview
if ([touch view] == self.customSubview){
// If it is, prevent all of the delegate's gesture recognizers
// from receiving the touch
return NO;
}
return YES;
}
Of course in this case customSubview would be subview on the page that has buttons in it (or even the buttons on it)
Swift 3
Set UITapGestureRecognizer
let tap = UITapGestureRecognizer(target: self, action: #selector(Class.didTap))
tap.delegate = self
UITapGestureRecognizer delegate method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return touch.view != buttonThatShouldCancelTapGesture
}
You can subclass UIView and overwrite the -touchesBegan selector, or you can play with the opaque property of the subviews to make them "invisible" to the touches (if view.opaque = NO, the view ignores touch events).
Related
I have two IBActions applied to a button one of which fires on touchdown and down and the other on touch up, however I also have a tap gesture applied to the button that fires on a double tap.
I've applied NSLog to see whats happening and the result is that both the touchdown and double tap fire when I double tap (fortunately the touch up doesn't fire) which makes sense - but how do i prevent the touch down firing when I double tap?
Code example
//fired on touch down
-(IBAction)touchdown:(id)sender
{
NSLog(#"touching down.");
}
//fired on touch up
-(IBAction)touchup:(id)sender
{
NSLog(#"taking off.");
}
//fired on double click
-(IBAction)boost:(UITapGestureRecognizer *)sender
{
NSLog(#"goodbye.");
}
Import the delegate, <UIGestureRecognizerDelegate>
I think you done this in viewDidLoad Method,
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(boost:)];
tapGestureRecognizer.delegate = self;
tapGestureRecognizer.numberOfTapsRequired = 2;
[self.button addGestureRecognizer:tapGestureRecognizer];
You need to implement this delegate method, based on your touch down coordinates,
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
CGPoint coords = [touch locationInView:self.button];
NSLog(#"Coords: %g, %g", coords.x, coords.y);
if (coords.y < 21 && coords.y > 25)
return NO;
else
return YES;
}
I added a UIPanGestureRecognizerto my UIButton. And now my when I drag a short distance with my finger after I pressed the button, the button stops being highlighted. Normally, a UIButton will stay highlighted unless you drag pretty far outside the UIButtons frame. However, with a pan gesture recognizer, now even if I drag a little bit, the button stops being highlighted.
Nothing in my code sets the buttons highlighted property to no. I even tried erasing all the code in my panning gesture recognizers action selector thing(the method that gets called whenever i pan on my button).
I also tried setting the button's highlighted property to NO in the panning gesture recognizer's action selector thing.
This kind of worked its just the highlightedness flashes. When you pan the highlighted goes away then comes back really fast, like a flash. So, this doesnt work too. Any ideas?
Gestures, by default, will cancel the touches in the views that they are linked to. So, when the touches in your button get cancelled it becomes unhighlighted. To prevent this behavior, set the cancelsTouchesInView property of your gesture to NO.
This will work
-(void) panDetected:(UIGestureRecognizer *) gesture
{
if(gesture.state == UIGestureRecognizerStateBegan)
{
yourButton.backgroundImageView.image = [UIImage imageNamed:#"your_button_highlited_image.png"];
}
else if(gesture.state == UIGestureRecognizerStateChanged)
{
}
else if(gesture.state == UIGestureRecognizerStateEnded)
{
yourButton.backgroundImageView.image = [UIImage imageNamed:#"your_button_normal_image.png"];
}
}
I have experienced the same problem. borrrden is right; to fix it, set the viewController the delegate for the gestureRecognizer, and cancel the touch if the touch is on a IUButton. First add the protocol `UIGestureRecognizerDelegate on the .h file. Then, after creating the gesture recognizer, set the view controller as its delegate:
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(aMethod)];
gestureRecognizer.delegate = self;
[self.view addGestureRecognizer:gestureRecognizer];
[gestureRecognizer release];
Finally override shouldReceiveTouchand cancel unwanted touches:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
if ([touch.view isKindOfClass:[UIButton class]]) { //Do not override UIButton touches
return NO;
}
return YES;
}
I'm using UIGestureRecognizer in my iOS application and I'm having some issues.
I only want the gestures to work in a certain area of the view, so I made a new UIView with a specific frame and added it to the root view. The gestures are working fine with this, but the only issue now is that I can't click the stuff that is under/behind that new view (the objects that are on the root view). If I set userInteractionEnabled to NO, it breaks the gestures so that is not an option.
What can I do to fix that?
Thanks.
Don't create a new view for your gesture recognizer. The recognizer implements a locationInView: method. Set it up for the view that contains the sensitive region. On the handleGesture, hit-test the region you care about like this:
0) Do all this on the view that contains the region you care about. Don't add a special view just for the gesture recognizer.
1) Setup mySensitiveRect
#property (assign, nonatomic) CGRect mySensitiveRect;
#synthesize mySensitiveRect=_mySensitiveRect;
self.mySensitiveRect = CGRectMake(0.0, 240.0, 320.0, 240.0);
2) Create your gestureRecognizer:
gr = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[self.view addGestureRecognizer:gr];
// if not using ARC, you should [gr release];
// mySensitiveRect coords are in the coordinate system of self.view
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer {
CGPoint p = [gestureRecognizer locationInView:self.view];
if (CGRectContainsPoint(mySensitiveRect, p)) {
NSLog(#"got a tap in the region i care about");
} else {
NSLog(#"got a tap, but not where i need it");
}
}
The sensitive rect should be initialized in myView's coordinate system, the same view to which you attach the recognizer.
Yo can also do:
gestureRecognizer.delegate = self
somewhere. generally on viewDidLoad(). then you implement the method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let view = self.getTheViewDontWannaConsider() /* or whateva */
let point = touch.location(in:view)
if point.y >= 50 /* or whateva calc. you want */ {
return false
}
return true
}
I have a bunch of UIViews like in the image below. The red/pink (semi-transparent) view is on top of the others.
Red has a UISwipeGestureRecognizer.
Green has as a UITapGestureRecognizer.
Blue has no recognizer.
A tap on the visible (bottom-left) part of Green trigger its recognizer.
A tap on the hidden parts of Green does not trigger its recognizer (Red blocks it).
That's the problem: I want Green to trigger. How can I do this?
In practice, the views may be in any order, any number and be subviews of each others etc. But the problem is the same:
How can I reliably find the uppermost view that can handle the gesture (tap or swipe)?
I tried with the code below. It neatly traverses all views, but it fails since it cannot know if the event is part of a swipe or a tap. So the method always returns the red view. If I remove the swipe-recognizer from Red, the code works correctly.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
if (self.hasASwipeRecognizer)
return self; // What if this was a tap?
if (self.hasATapRecognizer)
return self;
else
return nil;
}
else
return hitView;
}
An alternative to adding the gesture recognizer to these views would be to add the gesture recognizers to the parent view and handle the use cases appropriately using the delegate method gestureRecognizer:shouldReceiveTouch: method.
Identify whether the particular recognizer should receive the touch and return YES. For example, if the gesture recognizer passed is a swipe recognizer then check if the touch point is within the green view and return YES. Return NO otherwise.
If there are similar gesture recognizers then I suggest that you keep a reference and verify against it.
Usage
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint pointInView = [touch locationInView:gestureRecognizer.view];
if ( [gestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]]
&& CGRectContainsPoint(self.blueView.frame, pointInView) ) {
return YES;
}
if ( [gestureRecognizer isMemberOfClass:[UISwipeGestureRecognizer class]]
&& CGRectContainsPoint(self.greenView.frame, pointInView) ) {
return YES;
}
return NO;
}
One possible solution would be to add a tap gesture recognizer to the top red view and then whenever you get the tap, check whether the tap point intersects with the green view. If so, forward the tap to that view. If not, ignore the tap.
My solutions is:
-(void)handleGesture:(UIGestureRecognizer*)gestureRecognizer {
CGPoint touchPoint = [tapGestureRecognizer locationInView:viewUnderTest];
if ([viewUnderTest pointInside:touchPoint withEvent:nil]) {
NSLog(#"Hit done in view under test");
}
}
I have a UIButton that i'd like the user to be able to drag with TouchDragInside. How do i get the button to move as the user moves their finger?
As Jamie noted, a pan gesture recognizer is probably the way to go. The code would look something like what follows.
The button's view controller might add a gesture recognizer to the button (possibly in viewDidLoad) as follows:
UIPanGestureRecognizer *pangr = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
[myButton addGestureRecognizer:pangr];
[pangr release];
And, the view controller would have the following target method to handle the gesture:
- (void)pan:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateChanged ||
recognizer.state == UIGestureRecognizerStateEnded) {
UIView *draggedButton = recognizer.view;
CGPoint translation = [recognizer translationInView:self.view];
CGRect newButtonFrame = draggedButton.frame;
newButtonFrame.origin.x += translation.x;
newButtonFrame.origin.y += translation.y;
draggedButton.frame = newButtonFrame;
[recognizer setTranslation:CGPointZero inView:self.view];
}
}
CORRECTED as per rohan-patel's comment.
In the previously posted code , the x and y coordinate's of the origin of the button's frame were set directly. It was incorrect as: draggedButton.frame.origin.x += translation.x. A view's frame can be changed, but the frame's components cannot be changed directly.
You probably don't want to use TouchDragInside. That is a method of recognizing that a button or other control has been activated in a certain way. To move the button, you probably want to use a UIPanGestureRecognizer and then change the buttons position in its superview as the users finger moves around.
You have to implement these four methods, touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCancelled:withEvent: in the view that holds the button. The property you are referring to cannot be used directly to drag any uiview