Iphone SDK dismissing Modal ViewControllers on ipad by clicking outside of it
Asked Answered
S

14

55

I want to dismiss a FormSheetPresentation modal view controller when the user taps outside the modal view...I have seen a bunch of apps doing this (ebay on ipad for example) but i cant figure out how since the underneath views are disabled from touches when modal views are displayed like this (are they presenting it as a popover perhaps?)...anyone have any suggestions?

Sello answered 12/4, 2010 at 15:58 Comment(0)
F
128

I'm a year late, but this is pretty straightforward to do.

Have your modal view controller attach a gesture recognizer to the view's 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 release];

The handling code:

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

That's about it. HIG be damned, this is a useful and often intuitive behavior.

Feck answered 30/5, 2011 at 20:40 Comment(16)
I have a modalViewController with navigationController and this codes isn't working. I have also tried to pair gesture event to self.view.parentViewController.view with no luck. Need further help...Sarmentose
Test to put the UITapGestureRecognizer initialization code in viewDidAppear:Liatrice
1. It did not work when I initialized gestureRecognizer in viewDidLoad, but did work when I initialized in viewDidAppear.Rusch
2. ModalViews usually have delegates associated with them. Be sure to notify the delegate that you dismissed. Also, if there is any other way the modal view is dismissed, then you need a centralized method to remove the gesture recognizerRusch
Be aware of incompatibilities between iOS 4 and iOS 5 here; in iOS 5 the recognizer must be added in viewDidAppear, as mentioned above; on iOS 4 this is not necessary. Be aware also that the behaviour of parentViewController changed in iOS 5.Exuviae
I have to add - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; } (in the recognizer's delegate) to make other gesture recognizers workable.Anisole
working perfect for me :). I have used this code in Titanium appcelerator module, so, I need to make some modifications.Agitato
This has stopped working for me on iOS8 / XCode 6(4) !!Kirwin
In iOS 8, I use UIPresentationController instead, see WWDC 2014 code example:"LookInside: Presentation Controllers Adaptivity and Custom Animator Objects", add tap gesture in UIPresentationController's containerView.Edom
@Edom can you give a code example? Tried using [self.presentationController.containerView.window addGestureRecognizer:recognizer]; in viewDidLoad to no avail.Helmand
Change the location code to: "CGPoint location = [tapSender locationInView: self.presentingViewController.view];"Salmons
If your modal happens to have a UITableView, I recommend setting delaysTouchesEnded to NO on the gestureRecognizer to fix longer presses on cells (on iOS 7)Bidden
This also doesn't work if you place the code in viewWillAppearMckelvey
on iOS8 the delegate must be implemented and YES should be returned for gestureRecognizerShouldBegin, shouldRecognizeSimultaneouslyWithGestureRecognizer and shouldReceiveTouch.Likelihood
This works just fine in ios 9.2 and below. If you are having problems detecting the tap, add the gesture recognizer from storyboard or xib don't forget the cancel touches in viewDrear
What is the need for the [self.view.window removeGestureRecognizer:sender]; ? It works just as fine without it.Drear
H
12

For iOS 8, you must both implement the UIGestureRecognizer, 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;
}
Helmand answered 11/9, 2014 at 20:38 Comment(3)
I've found a problem with this code. If there is a button in the view and the keyboard is shown, when I click the button the UITapGestureRecognizer interprets wrong coordinate (everything has been moved when the keyboard appears) so the view gets dismissed.Rodger
hi i can't seems to detect tap outside the "presented" view, but able to detect tap within the "presented" viewWise
@Helmand I found that this works in the opposite way that it is supposed to. It detects taps inside the presented modal view but not outside it.Griffey
V
11

The other apps are not using Modal Views if they allow the view to be dismissed by clicking outside of it. UIModalPresentationFormSheets cannot be dismissed this way. (nor, indeed can any UIModal in SDK3.2). Only the UIPopoverController can be dismissed by clicking outside of the area. It is very possible (though against Apple's iPad HIG) for the app developer to have shaded out the background screen and then displayed the UIPopoverController so that it looks like a UIModalPresentationFormSheets (or other UIModal View).

[...] UIModalPresentationCurrentContext style lets a view controller adopt the presentation style of its parent. In each modal view, the dimmed areas show the underlying content but do not allow taps in that content. Therefore, unlike a popover, your modal views must still have controls that allow the user to dismiss the modal view.

See the iPadProgrammingGuide on the developer site for more information (Page 46 -- "Configuring the Presentation Style for Modal Views")

Videlicet answered 15/4, 2010 at 4:42 Comment(1)
Apple suggests a modal should contain its own close controls, but then contradicts that by having touch-outside-modal-to-dismiss within the iTunes app. the GAP app does the same thingGlyptograph
W
10

The code above works great, but I would change the if statement to,

    if (!([self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil] || [self.navigationController.view pointInside:[self.navigationController.view convertPoint:location fromView:self.navigationController.view.window] withEvent:nil]))

    {
        // Remove the recognizer first so it's view.window is valid.
        [self.view.window removeGestureRecognizer:sender];
        [self dismissModalViewControllerAnimated:YES];
    }

This makes sure you can still interact with the navigation bar, otherwise tapping in it dismisses the modal view.

Wooded answered 19/10, 2012 at 12:29 Comment(0)
N
10

Answer updated for iOS 8

Apparently, in iOS 8, the UIDimmingView has a tap gesture recognizer, which interferes with the initial implementation, so we ignore it and don't require it to fail.


This is the age of speed, so most are probably just copying the code above.. But, I suffer from OCD when it comes to code, unfortunately.

Here is a modular solution that uses Danilo Campos's answer with categories. It also solves an important bug that may occur if you are dismissing your modal through other means, as mentioned.

NOTE: The if statements are there because I use the view controller for both iPhone and iPad, and only the iPad needs to register/unregister.

UPDATE: The gist has been updated, since it didn't work properly with the awesome FCOverlay code, and it didn't allow gestures to be recognized in the presented view. Those issues are fixed. Using the category is as easy as:

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

    if (self.presentingViewController) {
        [self registerForDismissOnTapOutside];
    }
}

- (void)viewWillDisappear:(BOOL)animated
{
    if (self.presentingViewController) {
        [self unregisterForDismissOnTapOutside];
    }

    [super viewWillDisappear:animated];
}
Neu answered 8/5, 2014 at 13:12 Comment(2)
In your linked code, I would change the line [gesture locationInView:nil] to [gesture locationInView:view], then remove the line that converts the point. This seems to work better in iOS8.Bidden
@ArieLitovsky Thanks, you're absolutely right. Looking at my own project, I've already made that change, but didn't update the Gist since I started using project dependencies, like BlocksKit.Neu
D
8

Copy paste this code in your ModalViewController :

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    //Code for dissmissing this viewController by clicking outside it
    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];
        }
    }
}
Dramatics answered 17/9, 2012 at 7:21 Comment(2)
I meant that your answer is the same, comments included, as the approved one on this Q.Likelihood
viewDidAppear is my add. You can down vote. Moderators can see their job !Dramatics
C
3

Very important: If you have any other way to close your modal popup window, don't forget to remove the tap gesture recognizer!

I forgot this, and got crazy crashes later on, since the tap recognizer was still firing events.

Cylindrical answered 6/1, 2014 at 17:53 Comment(0)
L
2

Accoring to Apple's iOS HIG, 1. the modal view doesn't have that ability to be dismissed without any input on itself; 2. use modal view in the situation that a user input is required.

Laryngology answered 19/9, 2011 at 0:26 Comment(0)
E
1

Use UIPresentationController instead:

- (void)presentationTransitionWillBegin
{
    [super presentationTransitionWillBegin];
    UITapGestureRecognizer *dismissGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(dismissGestureTapped:)];
    [self.containerView addGestureRecognizer:dismissGesture];

    [[[self presentedViewController] transitionCoordinator] animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    } completion:nil];
}
- (void) dismissGestureTapped:(UITapGestureRecognizer *)sender{
    if (sender.state==UIGestureRecognizerStateEnded&&!CGRectContainsPoint([self frameOfPresentedViewInContainerView], [sender locationInView:sender.view])) {
        [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    }
}

Modified from LookInside example

Edom answered 12/9, 2014 at 11:55 Comment(0)
V
1

This works for me for ios7 an 8 and navigation bar.

If you don't need the nav bar just remove location2 and second condition in the if statement after the pipes.

@MiQUEL this should work for you too

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

        if (!([self.view pointInside:location1 withEvent:nil] || [self.navigationController.view pointInside:location2 withEvent:nil])) {
            [self.view.window removeGestureRecognizer:self.recognizer];
            [self dismissViewControllerAnimated:YES completion:nil];
        }
    }
}

Edit: You may also need to be a gesture recognizer delegate for this and other above solutions to work. Do it like so:

@interface CommentTableViewController () <UIGestureRecognizerDelegate>

set yourself as the delegate for the recognizer:

self.recognizer.delegate = self;

and implement this delegate method:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    return YES;
}
Vierra answered 10/10, 2014 at 5:34 Comment(0)
L
0

It is pretty doable.

Take a look here

https://mcmap.net/q/433167/-dismiss-modal-view-form-sheet-controller-on-outside-tap

It is a NavigationController (modal) that auto dismiss for ipad (when you tap outside)

Use your viewcontroller inside of it.

Hope it helps.

Leveller answered 24/9, 2014 at 12:25 Comment(0)
S
0

I know it's late but consider using CleanModal (tested with iOS 7 and 8).

https://github.com/orafaelreis/CleanModal

Salmons answered 17/11, 2014 at 16:36 Comment(0)
M
0

You can use MZFormSheetController like this:

MZFormSheetController *formSheet = [[MZFormSheetController alloc] initWithSize:customSize viewController:presentedViewController];
formSheet.shouldDismissOnBackgroundViewTap = YES;
[presentingViewController mz_presentFormSheetController:formSheet animated:YES completionHandler:nil];
Marciemarcile answered 22/1, 2015 at 11:5 Comment(0)
B
0

In Swift 2/Xcode Version 7.2 (7C68) the following code worked for me.

Attention: this code should be put in the ViewController.swift file of the presented FormSheet or Page Sheet, here: "PageSheetViewController.swift"

class PageSheetViewController: UIViewController, UIGestureRecognizerDelegate {

    override func viewDidAppear(animated: Bool) {
        let recognizer = UITapGestureRecognizer(target: self, action:Selector("handleTapBehind:"))
        recognizer.delegate = self
        recognizer.numberOfTapsRequired = 1
        recognizer.cancelsTouchesInView = false
        self.view.window?.addGestureRecognizer(recognizer)
    }

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

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

            // detect iOS Version 8.0 or greater
            let Device = UIDevice.currentDevice()
            let iosVersion = Double(Device.systemVersion) ?? 0
            let iOS8 = iosVersion >= 8

            if (iOS8) {
                // in landscape view you will have to swap the location coordinates
                if(UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication().statusBarOrientation)){
                    location = CGPointMake(location.y, location.x);
                }
            }

            if(!self.view.pointInside(self.view.convertPoint(location, fromView: self.view.window), withEvent: nil)){
                self.view.window?.removeGestureRecognizer(sender)
                self.dismissViewControllerAnimated(true, completion: nil)
            }
        }
    }
}
Baronet answered 24/1, 2016 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.