22
votes

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 ?

3
I've posted a link of the code I tried.mac_019_0
Post your related codes, not other people's codes. Also, didn't work is NOT a valid issue description.Raptor
Updated the question, Please take a look.mac_019_0
I haven't found any solution yet but i'm following this discussion: stackoverflow.com/questions/9102497/…Martino Bonfiglioli

3 Answers

38
votes

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];
        }];
    }
}
8
votes

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
    }
}
7
votes

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];
}