Dismiss modal form sheet view on outside tap iOS 8
Asked Answered
B

3

21

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 ?

Brakpan answered 3/9, 2014 at 7:14 Comment(4)
I've posted a link of the code I tried.Brakpan
Post your related codes, not other people's codes. Also, didn't work is NOT a valid issue description.Fallacious
Updated the question, Please take a look.Brakpan
I haven't found any solution yet but i'm following this discussion: #9102997Overthrow
B
38

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];
        }];
    }
}
Balaam answered 15/9, 2014 at 8:55 Comment(7)
I followed your steps, it worked perfectly for me. However, I'm not sure why we need add "shouldRecognizeSimultaneouslyWithGestureRecognizer:" at the first place. If I don't return "YES", tap out side of modal view will not be recognized. But why it has something to do with "RecognizeSimultaneouslyWithGestureRecognizer"?Lyophobic
@DavidLiu there is another (system) gesture recognizer active which takes priority. Probably a bug.Balaam
This is the best solution. And is solved the rotation coordinates issue that other solutions have. Perfect!Truant
I found that with this solution, tapping on the navbar would also discard it.Ouphe
Your solution is the same as mine, but as of iOS8 on small iPhones where the formsheet is presented fullscreen, landscape is throwing it all off. Somehow on iPhone the rotation of the coordinates isn't working at all. You can tap anywhere in the fullscreen view and it dismisses. But only in landscape. it still works perfectly on iPhone 6 Plus, iPad, iPhone portrait etc. Trying to find a better method of checking than just if (iphone) or something.Fewness
The best I've got so far is CGRectEqualToRect(self.view.bounds, rootView.bounds) to see if its fullscreen then return out.Fewness
In iOS 8.1 using almost exactly the same code I can get the handleTapBehind with self.view but not self.window. Could that be an iOS 8 .1 vs 8.0 difference? (Or else something in my app interferes?)Zahavi
C
8

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];
}
Circadian answered 15/9, 2014 at 10:48 Comment(4)
I'm guessing the down vote is because this tutorial only talks about animating the transitions and not about a tab-outside dismissal feature. Unclear whether UIPresentationController class does anything to address the question at hand (without reading the docs).Zahavi
Since UIPresentationController is the right approach I updated the answer to include the required tap to dismiss code sample. And Apple's example code is perfectly fine rather than that blog post: developer.apple.com/library/ios/featuredarticles/…Teyde
I prefer this approach myself, since I've had instances where a form sheet presents a popover view whose popover may draw outside the bounds of the presented form sheet — and the view.window-based gesture recognizer will dismiss the view if you tap the popover anywhere outside the bounds of the form sheet. I'd much rather the dimming view handle the tap!Navel
Still the best way to do it in iOS 12 when working with custom Modal Segue and UIPresentationController. Don't forget to add self.dimmingView setUserInteractionEnabled:YES or the gestureRecognizer won't catch anything.Plunger
P
7

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
    }
}
Proprietor answered 25/5, 2017 at 2:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.