Dismiss modal view form sheet controller on outside tap

|▌冷眼眸甩不掉的悲伤 提交于 2019-11-27 07:25:58

Ah ok. So I'm afraid thats not quite possible using the presentModalViewController: method. The whole idea of a "modal" view/window/message box/etc. pp. is that the user cannot do anything else than processing whatever the view/window/message box/etc. pp. wants him/her to do.

What you want to do instead is not present a modal view controller, but rather load and show your form view controller the regular way. Note in your master controller that the form is just showing e.g. with a BOOL variable and then handle there any taps that might occur. If your form is showing, dismiss it.

Ares

I know this is an old question but this IS possible, despite of what the "right" answer says. Since this was the first result when I was looking for this I decided to elaborate:

This is how you do it:

You need to add a property to the View Controller from where you want to present modally, in my case "tapBehindGesture".

then in viewDidAppear

if(!tapBehindGesture) {
        tapBehindGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapBehindDetected:)];
        [tapBehindGesture setNumberOfTapsRequired:1];
        [tapBehindGesture setCancelsTouchesInView:NO]; //So the user can still interact with controls in the modal view
    }

[self.view.window addGestureRecognizer:tapBehindGesture];

And Here is the implementation for tapBehindDetected

- (void)tapBehindDetected:(UITapGestureRecognizer *)sender
{

    if (sender.state == UIGestureRecognizerStateEnded)
    {
        //(edited) not working for ios8 above 
        //CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window

        CGPoint location = [sender locationInView: self.presentingViewController.view];

        //Convert tap location into the local view's coordinate system. If outside, dismiss the view.
        if (![self.presentedViewController.view pointInside:[self.presentedViewController.view convertPoint:location fromView:self.view.window] withEvent:nil])
        {   
            if(self.presentedViewController) {
                [self dismissViewControllerAnimated:YES completion:nil];
            }
        }
    }
}

Just remember to remove tapBehindGesture from view.window on viewWillDisappear to avoid triggering handleTapBehind in an unallocated object.

I solved iOS 8 issue by adding delegate to gesture recognizer

[taprecognizer setDelegate:self];

with these responses

#pragma mark - UIGestureRecognizer Delegate

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

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

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    return YES;
}

that works for me with iOS 8 GM

As far as I can tell none of the answer seem to be working right away in every condition.

My solution (either inherit from it or paste it in):

@interface MyViewController () <UIGestureRecognizerDelegate>

@property (strong, nonatomic) UITapGestureRecognizer *tapOutsideRecognizer;

@end

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    if (!self.tapOutsideRecognizer) {
        self.tapOutsideRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapBehind:)];
        self.tapOutsideRecognizer.numberOfTapsRequired = 1;
        self.tapOutsideRecognizer.cancelsTouchesInView = NO;
        self.tapOutsideRecognizer.delegate = self;
        [self.view.window addGestureRecognizer:self.tapOutsideRecognizer];
    }
}

-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    // to avoid nasty crashes
    if (self.tapOutsideRecognizer) {
        [self.view.window removeGestureRecognizer:self.tapOutsideRecognizer];
        self.tapOutsideRecognizer = nil;
    }
}

#pragma mark - Actions 

- (IBAction)close:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (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 close:sender];
        }
    }
}

#pragma mark - Gesture Recognizer
// because of iOS8
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    return YES;
}

Here is my version that works for iOS 7 and iOS 8 and does not require conditional swapping of coordinates:

- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateEnded)
    {
        CGPoint location = [sender locationInView:self.view];

        if (![self.view pointInside:location withEvent:nil]) {
            [self.view.window removeGestureRecognizer:self.recognizer];
            [self dismissViewControllerAnimated:YES completion:nil];
        }
    }
}
Jim Conroy

Swift 4 version that works in both portrait and landscape - no swapping of x, y required.

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
@objc 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
}

}

For iOS 8, you must both implement the UIGestureRecognizer per Martino's answer, and swap the (x,y) coordinates of the tapped location when in landscape orientation. Not sure if this is due to an iOS 8 bug.

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // add gesture recognizer to window

    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];
    recognizer.delegate = self;
}

- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateEnded) {

        // passing nil gives us coordinates in the window
        CGPoint location = [sender locationInView:nil];

        // swap (x,y) on iOS 8 in landscape
        if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
            if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
                location = CGPointMake(location.y, location.x);
            }
        }

        // 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 dismissViewControllerAnimated:YES completion:nil];
        }
    }
}


#pragma mark - UIGestureRecognizer Delegate

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

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

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    return YES;
}
neoneye

Based on Bart van Kuik's answer and NavAutoDismiss and other great snippets here.

class DismissableNavigationController: UINavigationController, UIGestureRecognizerDelegate {
    private var tapOutsideRecognizer: UITapGestureRecognizer!

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)

        if tapOutsideRecognizer == nil {
            tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: #selector(DismissableNavigationController.handleTapBehind))
            tapOutsideRecognizer.numberOfTapsRequired = 1
            tapOutsideRecognizer.cancelsTouchesInView = false
            tapOutsideRecognizer.delegate = self
            view.window?.addGestureRecognizer(tapOutsideRecognizer)
        }
    }

    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)

        if tapOutsideRecognizer != nil {
            view.window?.removeGestureRecognizer(tapOutsideRecognizer)
            tapOutsideRecognizer = nil
        }
    }

    func close(sender: AnyObject) {
        dismissViewControllerAnimated(true, completion: nil)
    }

    func handleTapBehind(sender: UITapGestureRecognizer) {
        if sender.state == UIGestureRecognizerState.Ended {
            var location: CGPoint = sender.locationInView(nil)

            if UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication().statusBarOrientation) {
                location = CGPoint(x: location.y, y: location.x)
            }

            if !view.pointInside(view.convertPoint(location, fromView: view.window), withEvent: nil) {
                view.window?.removeGestureRecognizer(sender)
                close(sender)
            }
        }
    }

    func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

Usage:

let vc = MyViewController()
let nc = DismissableNavigationController(rootViewController: vc)
nc.modalPresentationStyle = UIModalPresentationStyle.FormSheet
presentViewController(nc, animated: true, completion: nil)
Bart van Kuik

@yershuachu's answer, in Swift 2:

class ModalParentViewController: UIViewController, UIGestureRecognizerDelegate {

    private var tapOutsideRecognizer: UITapGestureRecognizer!

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)

        if(self.tapOutsideRecognizer == nil) {
            self.tapOutsideRecognizer = UITapGestureRecognizer(target: self, action: "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.dismissViewControllerAnimated(true, completion: nil)
    }

    func handleTapBehind(sender: UITapGestureRecognizer) {
        if (sender.state == UIGestureRecognizerState.Ended) {
            let location: CGPoint = sender.locationInView(nil)

            if (!self.view.pointInside(self.view.convertPoint(location, fromView: self.view.window), withEvent: nil)) {
                self.view.window?.removeGestureRecognizer(sender)
                self.close(sender)
            }
        }
    }

    func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

}

Swift 3

class ModalParentViewController: 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
        appDelegate.window?.addGestureRecognizer(self.tapOutsideRecognizer)
    }
}

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)

      if(self.tapOutsideRecognizer != nil) {
        appDelegate.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: nil)

        if (!self.view.point(inside: self.view.convert(location, from: self.view.window), with: nil)) {
            self.view.window?.removeGestureRecognizer(sender)
            self.close(sender: sender)
        }
    }
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

}

I use it in this form without any problems neither on iOS 7.1 nor iOS 8.3.

- (void)viewDidAppear:(BOOL)animated
{
     [super viewDidAppear:animated];

     [self.view.window addGestureRecognizer:self.tapBehindGesture];
}

- (void)viewWillDisappear:(BOOL)animated
{
     [super viewWillDisappear:animated];

     [self.view.window removeGestureRecognizer:self.tapBehindGesture];
}

- (UITapGestureRecognizer*)tapBehindGesture
{    
    if (_tapBehindGesture == nil)
    {
        _tapBehindGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapBehindRecognized:)];
        _tapBehindGesture.numberOfTapsRequired = 1;
        _tapBehindGesture.cancelsTouchesInView = NO;
        _tapBehindGesture.delegate = self;
    }

    return _tapBehindGesture;
}

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

I made a navigationController that auto dismiss for iPad

https://github.com/curciosobrinho/NavAutoDismiss

It is just the same code as above, working on iOS 8.

So, all the credits go to the people above.

I just did it to be more generic and easier to use.

You just need to copy BOTH files to your project

Import the header file (.h)

And use your viewcontroller (that you want to show) as the rootViewController.

How to use it:

//Import the .h file
#import "NavDismissViewController.h"

//Instanciate your view controller (the view you want to present)

YourViewController * yourVC = [YourViewController new];

//Instanciate the NavDismissViewController with your view controller as the rootViewController

NavDismissViewController *nav = [[NavDismissViewController alloc] initWithRootViewController:yourVC];

//if you want to change the navigationBar translucent behaviour

[nav.navigationBar setTranslucent:NO];

//Choose the Modal style

nav.modalPresentationStyle=UIModalPresentationFormSheet;

//present your controller

[self presentViewController:nav animated:YES completion:nil];

//Done
extension CGPoint {
    mutating func correctOrientation() {
        let screenSize = UIScreen.mainScreen().bounds
        switch(UIDevice.currentDevice().orientation) {
        case .Portrait:
            break
        case .PortraitUpsideDown:
            y = screenSize.height - y
            break
        case .LandscapeLeft:
            swap(&y, &x)
            y = screenSize.height - y
            break
        case .LandscapeRight:
            swap(&y, &x)
            x = screenSize.width - x
            break
        default:
            break
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!