exclude subview from UITapGestureRecognizer - ios

I have a subview and a superview. The superview has an UITapGestureRecognizer attached to it.
UIView *superview = [[UIView alloc] initWithFrame:CGRectMake:(0, 0, 320, 480);
UIView *subview = [[UIView alloc] initWithFrame:CGRectMake:(100, 100, 100, 100);
UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget: self action: #selector(handleTap);
superview.userInteractionEnabled = YES;
subview.userInteractionEnabled = NO;
[superview addGestureRecognizer:recognizer];
[self addSubview:superview];
[superview addSubview:subview];
The recognizer is fired inside the subview as well, is there a way to exclude the recognizer from the subview?
I know this question has been asked before but I didn't find a good answer to it.

You can use gesture recognizer delegate to limit area where it can recognise touches similar to this example:
recognizer.delegate = self;
...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
CGPoint touchPoint = [touch locationInView:superview];
return !CGRectContainsPoint(subview.frame, touchPoint);
}
Note that you need to keep reference to your parent and child view (make them instance variables?) to be able to use them in delegate method

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if(touch.view == yourSubview)
{
return NO;
}
else
{
return YES;
}
}
Thanks : https://stackoverflow.com/a/19603248/552488

For Swift 3, you can use view.contains(point) instead of CGRectContainsPoint.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if yourSubview.frame.contains(touch.location(in: view)) {
return false
}
return true
}

Related

Run action if a existing screen interaction passes over and stops on a UIView

How do I add an action to a uiview that will run if a screen interaction (long press) moves to a uiview and then stays there for a select period of time.
EDIT: I have a drop down menu sort of system and a long press on the button reveals the menu. Then a user would slide down onto an option and then release the tap. I want to tell which menu item that happened on and act accordingly.
Add the below gesture to your UIView for long press functionality.
var longTap = UILongPressGestureRecognizer(target: self, action: #selector(self.longTouch))
longTap.numberOfTapsRequired = 0 // Set your own number here
longTap.minimumPressDuration = 1.0 //Set your duration here
longTap.delegate = self// Add the <UIGestureRecognizerDelegate> protocol
self.view.addGestureRecognizer(longTap)
Add the delegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Below is selector will be called
func longTouch(_ recognizer: UILongPressGestureRecognizer) {
if recognizer.state == .began {
print("longTouch UIGestureRecognizerStateBegan")
}
if recognizer.state == .ended {
print("longTouch UIGestureRecognizerStateEnded")
}
}
Objective C version
UILongPressGestureRecognizer *longTap = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longTouch:)];
[longTap setNumberOfTapsRequired:0]; // Set your own number here
[longTap setMinimumPressDuration:1.0]; // Set your duration here
[longTap setDelegate:self]; // Add the <UIGestureRecognizerDelegate> protocol
[self.view addGestureRecognizer:longTap];
Delgate:-
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Selector:-
- (void) longTouch: (UILongPressGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
NSLog(#"longTouch UIGestureRecognizerStateBegan");
}
if (recognizer.state == UIGestureRecognizerStateEnded)
{
NSLog(#"longTouch UIGestureRecognizerStateEnded");
}
}

How do I prevent taps on a subview from triggering a UITapGestureRecognizer on a parent view?

I added a tap recognizer to a view:
UITapGestureRecognizer* tgr = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector( onTap )];
[view addGestureRecognizer: tgr];
The problem is that taps on subviews of view trigger onTap. How do I prevent that?
Suppose your parentView has a subView. You implement the following UIGestureRecognizerDelegate method, if the touch is inside the bounds of subView, you return no.
tgr.delegate = self;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
CGPoint locationInView = [touch locationInView:self.parentView];
if (CGRectContainsPoint(self.subView.frame, locationInView) ) {
return NO;
} else {
return YES;
}
}
Add subview to the background of the view, and attach tap gesture recogniser to the subview:
UIView* subview = [[UIView alloc] initWithFrame:view.bounds];
subview.backgroundColor = [UIColor clearColor];//or view.backgroundColor
[view addSubview:subview];
[view sendSubviewToBack:subview];
[subview addGestureRecognizer:tapRecognizer];
Swift 4.2 answer for an AlertView
Add the gesture recogniser and set the delegate:
let backgroundViewTapGesture = UITapGestureRecognizer(target: self, action: #selector(dismiss))
backgroundViewTapGesture.delegate = self
self.backgroundView.addGestureRecognizer(backgroundViewTapGesture)
self.backgroundViewTapGesture = backgroundViewTapGesture
Then add the extension to handle the tap
extension AlertView: UIGestureRecognizerDelegate {
// Only handle this for the background tap gesture
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard gestureRecognizer == backgroundViewTapGesture, let backgroundView = gestureRecognizer.view, let alertView = self.alertContentView else {
return true
}
let touchLocation = touch.location(in: backgroundView)
return !alertView.frame.contains(touchLocation)
}
}
In Swift:
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let locationInView = touch.location(in: mainView)
if debugOptionsView.isHidden {
return true
}
return !debugOptionsView.frame.contains(locationInView)
}

Dismiss modal form sheet view on outside tap iOS 8

I've been trying to dismiss the modal form sheet view on outside tap on iOS 8 with no luck,
I've tried this code
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view.window addGestureRecognizer:recognizer];
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
// Remove the recognizer first so it's view.window is valid.
[self.view.window removeGestureRecognizer:sender];
[self dismissModalViewControllerAnimated:YES];
}
}
}
But it doesn't detect outside view clicks, any suggestions ?
There are actually two problems in iOS 8. First, the gesture recognition does not begin.
I solved this by adding the UIGestureRecognizerDelegate protocol and implementing
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
{
return YES;
}
Also, don't forget to register the delegate with
recognizer.delegate = self;
Now the gesture recognizer should recognize gestures and the target method (handleTapBehind:) will be called.
Here comes the second problem in iOS 8: locationInView: doesn't seem to take the device orientation into account if nil is passed as a view. Instead, passing the root view works.
Here's my target code that seems to work for iOS 7.1 and 8.0:
if (sender.state == UIGestureRecognizerStateEnded) {
UIView *rootView = self.view.window.rootViewController.view;
CGPoint location = [sender locationInView:rootView];
if (![self.view pointInside:[self.view convertPoint:location fromView:rootView] withEvent:nil]) {
[self dismissViewControllerAnimated:YES completion:^{
[self.view.window removeGestureRecognizer:sender];
}];
}
}
In iOS 8, You can look at using the new UIPresentationController class. It gives you better control over the container around your custom view controller presentation (allowing you to correctly add a gesture recogniser of your own).
Here is a link to quite a simple tutorial as well: http://dativestudios.com/blog/2014/06/29/presentation-controllers/
Then add the dimming view tap-to-dismiss:
UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
[self.dimmingView addGestureRecognizer:singleFingerTap];
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
Swift 3.1 solution that works in both portrait and landscape.
class TapBehindModalViewController: UIViewController, UIGestureRecognizerDelegate {
private var tapOutsideRecognizer: UITapGestureRecognizer!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if(self.tapOutsideRecognizer == nil) {
self.tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTapBehind))
self.tapOutsideRecognizer.numberOfTapsRequired = 1
self.tapOutsideRecognizer.cancelsTouchesInView = false
self.tapOutsideRecognizer.delegate = self
self.view.window?.addGestureRecognizer(self.tapOutsideRecognizer)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if(self.tapOutsideRecognizer != nil) {
self.view.window?.removeGestureRecognizer(self.tapOutsideRecognizer)
self.tapOutsideRecognizer = nil
}
}
func close(sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
// MARK: - Gesture methods to dismiss this with tap outside
func handleTapBehind(sender: UITapGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.ended) {
let location: CGPoint = sender.location(in: self.view)
if (!self.view.point(inside: location, with: nil)) {
self.view.window?.removeGestureRecognizer(sender)
self.close(sender: sender)
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

iOS MapView: Add Annotation on tap, but not if existing Annotation Tapped

I have a UITapGestureRecognizer setup to add an annotation onto the map where the user clicks. The problem I'm running into is when the user taps an existing annotation to view the tooltip, the tooltip pops up, but another annotation is added to the map behind the clicked annotation. Is there a way to detect if an annotation was tapped and return before adding the annotation?
This is my viewDidLoad:
UITapGestureRecognizer *singleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(foundTap:)];
singleTapRecognizer.numberOfTapsRequired = 1;
[self.mapView addGestureRecognizer:singleTapRecognizer];
And my on touch function:
-(IBAction)foundTap:(UITapGestureRecognizer *)recognizer
{
CGPoint point = [recognizer locationInView:self.mapView];
CLLocationCoordinate2D tapPoint = [self.mapView convertPoint:point toCoordinateFromView:self.view];
AGIAnnotation * annotation = [[AGIAnnotation alloc] initWithCoordinate:tapPoint];
// Some other stuff
[self.mapView addAnnotation:annotation];
}
Any help is appreciated.
There is a UIGestureRecognizerDelegate Protocol
Implement gestureRecognizer:shouldReceiveTouch: and return NO if the touch is on an existing tooltip. Something like this:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([touch.view isKindOfClass:[MKPinAnnotationView class]])
{
return NO;
}
return YES;
}
#mbehan answer in Swift 3+:
in ViewDidLoad:
let singleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(YourClass.foundTap(_:)))
singleTapRecognizer.delegate = self
mapView.addGestureRecognizer(singleTapRecognizer)
and the UIGestureRecognizerDelegate extension:
extension YourClass: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return !(touch.view is MKPinAnnotationView)
}
}

UIScrollview limit swipe area

I am trying to limit the swipe area of the UIScrollview, but i amnot able to do that.
I would like to set the swipe area only to the top of the UIScrollview, but i would like to set all the content visible.
Update:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if ([touches count] > 0) {
UITouch *tempTouch = [touches anyObject];
CGPoint touchLocation = [tempTouch locationInView:self.categoryScrollView];
if (touchLocation.y > 280.0)
{
NSLog(#"enabled");
self.categoryScrollView.scrollEnabled = YES;
}
}
[self.categoryScrollView touchesBegan:touches withEvent:event];
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// [super touchesEnded:touches withEvent:event];
self.categoryScrollView.scrollEnabled = YES;
[self.categoryScrollView touchesBegan:touches withEvent:event];
}
Solution:
dont forget to set delaysContentTouches to NO on the UIScrollView
self.categoryScrollView.delaysContentTouches = NO;
You can disable scrolling on the UIScrollView, override touchesBegan:withEvent: in your view controller, check if any of the touches began in the area where you'd like to enable swipes, and if the answer is 'yes', re-enable scrolling. Also override touchesEnded:withEvent: and touchesCancelled:withEvent: to disable scrolling when the touches are over.
Other answers didn't work for me. Subclassing UIScrollView worked for me (Swift 3):
class ScrollViewWithLimitedPan : UIScrollView {
// MARK: - UIPanGestureRecognizer Delegate Method Override -
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let locationInView = gestureRecognizer.location(in: self)
print("where are we \(locationInView.y)")
return locationInView.y > 400
}
}
This blog post showcases a very simple and clean way of implementing the functionality.
// init or viewDidLoad
UIScrollView *scrollView = (UIScrollView *)view;
_scrollViewPanGestureRecognzier = [[UIPanGestureRecognizer alloc] init];
_scrollViewPanGestureRecognzier.delegate = self;
[scrollView addGestureRecognizer:_scrollViewPanGestureRecognzier];
//
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
{
return NO;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer == _scrollViewPanGestureRecognzier)
{
CGPoint locationInView = [gestureRecognizer locationInView:self.view];
if (locationInView.y > SOME_VALUE)
{
return YES;
}
return NO;
}
return NO;
}

Resources