2

I have been trying to implement a UITapGestureRecognizer that will dismiss a modal view controller by detecting a tap outside the modal view controller on iOS 8.

The issue I am seeing is that in landscape mode on iPad that the tap gesture is only recognised inside parts of the view controller. My modal view is 380 width x 550 height. When orientation is in landscape (with the home button at the right) the tap gesture is detected inside the bottom half of the modal view. When orientation is in landscape but with the home button at the left hand side the tap gesture is detected inside the top half of the modal view.

Problem 1: The tap gesture is only detected inside the modal view when it should be detected outside. Problem 2: No detection in some areas of the view in landscape orientation.

Here is the code I used:

UIViewController+DismissOnTapOutside.h

#import <UIKit/UIKit.h>

@interface UIViewController (DismissOnTapOutside)

- (void)registerForDismissOnTapOutside; // Call in viewDidAppear
- (void)unregisterForDismissOnTapOutside; // Call in viewWillDisappear

@end

UIViewController+DismissOnTapOutside.m

#import "UIViewController+DismissOnTapOutside.h"
#import <objc/runtime.h>

static char gestureRecognizerKey;
static char gestureRecognizerDelegateKey;

@interface SimpleGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate>

@end

@implementation SimpleGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{

    return [otherGestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]];
}

@end

@interface UIViewController ()

@property (assign, nonatomic) UIGestureRecognizer *gestureRecognizer;
@property (strong, nonatomic) SimpleGestureRecognizerDelegate *gestureRecognizerDelegate;

@end

@implementation UIViewController (DismissOnTapOutside)

- (void)setGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
    objc_setAssociatedObject(self, &gestureRecognizerKey, gestureRecognizer, OBJC_ASSOCIATION_ASSIGN);
}

- (UIGestureRecognizer *)gestureRecognizer
{
    return objc_getAssociatedObject(self, &gestureRecognizerKey);
}

- (void)setGestureRecognizerDelegate:(SimpleGestureRecognizerDelegate *)delegate
{
    objc_setAssociatedObject(self, &gestureRecognizerDelegateKey, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIGestureRecognizer *)gestureRecognizerDelegate
{
    id delegate = objc_getAssociatedObject(self, &gestureRecognizerDelegateKey);
    if (!delegate)
    {
        delegate = [[SimpleGestureRecognizerDelegate alloc] init];
        self.gestureRecognizerDelegate = delegate;
    }

    return delegate;
}

- (void)handleDismissTap:(UIGestureRecognizer *)gesture
{
    if (gesture.state == UIGestureRecognizerStateEnded)
    {
        UIView *view = self.navigationController.view ?: self.view;
        // Passing nil gives us coordinates in the window
        CGPoint location = [gesture locationInView:nil];

        if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
            if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
                location = CGPointMake(location.y, location.x);
            }
        }

        // Then we convert the tap's location into the local view's coordinate system
        location = [view convertPoint:location fromView:self.view.window];

        if (![view pointInside:location withEvent:nil])
        {
            [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
        }
    }
}

- (void)registerForDismissOnTapOutside
{
    // This approach is attributed to Danilo Campos:
    // http://stackoverflow.com/a/6180584/456434
    UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissTap:)];
    recognizer.delegate = self.gestureRecognizerDelegate;
    recognizer.numberOfTapsRequired = 1;
    recognizer.cancelsTouchesInView = NO;

    self.gestureRecognizer = recognizer;
    [self.view.window addGestureRecognizer:recognizer];
}

- (void)unregisterForDismissOnTapOutside
{
    [self.view.window removeGestureRecognizer:self.gestureRecognizer];
    self.gestureRecognizer = nil;
}

@end

The key method that should set the correct location for tap detection does not seem to be setting the location correctly in landscape orientation in iOS 8:

- (void)handleDismissTap:(UIGestureRecognizer *)gesture
{
    if (gesture.state == UIGestureRecognizerStateEnded)
    {
        UIView *view = self.navigationController.view ?: self.view;
        // Passing nil gives us coordinates in the window
        CGPoint location = [gesture locationInView:nil];

        if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
            if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
                location = CGPointMake(location.y, location.x);
            }
        }

        // Then we convert the tap's location into the local view's coordinate system
        location = [view convertPoint:location fromView:self.view.window];

        if (![view pointInside:location withEvent:nil])
        {
            [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
        }
    }
}

All of the examples I have looked at on StackOverflow in relation to this issue are adamant that the above code works. But in my case it simply does not detect taps outside the modal view controller.

How can a tap outside a modal be detected in iOS 8 on iPad in landscape orientation?

motionpotion
  • 2,656
  • 6
  • 42
  • 60

0 Answers0