28

Previous to iOS8 we used the UIActionSheet for showing alert and now we need to use the UIAlertController.

When we used the UIActionSheet we could easily handle situations where the user clicked outside the pop up (which means he want to cancel the operation) by comparing the clickedButtonAtIndex to the cancelButtonIndex - if the user indeed pressed outside the popup we got the cancel button index in this function.

How can we handle these situations with the new UIAlertController? I tried to use the "completion" block but it doesn't have any context. Is there an easy way to handle this? (other than "saving" the actions states in some general variable).

Tomer Peled
  • 3,571
  • 5
  • 35
  • 57

3 Answers3

45

You can add an action with style:UIAlertActionStyleCancel and the handler for this action is called when the user taps outside the popup.

if ([UIAlertController class]) {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert Title" message:@"A Message" preferredStyle:UIAlertControllerStyleActionSheet];

    [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
        NSLog(@"User clicked button called %@ or tapped elsewhere",action.title);
    }]];

    [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
        NSLog(@"User clicked button called %@",action.title);
    }]];

    [alertController addAction:[UIAlertAction actionWithTitle:@"Other" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) {
        NSLog(@"User clicked button called %@",action.title);
    }]];

    UIControl *aControl = (UIControl *) sender;
    CGRect frameInView = [aControl convertRect:aControl.bounds toView:self.view];
    alertController.popoverPresentationController.sourceRect = frameInView;
    alertController.popoverPresentationController.sourceView = self.view;
    alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionAny;
    [self presentViewController:alertController animated:YES completion:nil];
}
Gareth
  • 2,051
  • 2
  • 19
  • 14
  • Perhaps I'm missing something (I'm very new to iOS). But in my experience, using UIAlertControllerStyleActionSheet with UIAlertActionStyleCancel seems not to add the Cancel action. Changing the style to UIAlertControllerStyleAlert always shows the cancel action. – Joey Carson Nov 19 '14 at 17:14
  • Note that the UIAlertController will remove the cancel button when using a popover, even if you add it as an action. A user cancels a popover by touching outside of the popover so that it is not required. – Reefwing Feb 05 '15 at 03:27
  • 1
    Note: Having a UIAlertActionStyleCancel styled action only dismisses the AlertController if you are using the UIAlertControllerStyleActionSheet AlertController style. It does not work with the UIAlertControllerStyleAlert. – micnguyen Apr 24 '15 at 03:43
  • @JDG Yeah but the alert style can't be canceled with an outside tap anyway so it doesn't matter. – Supertecnoboff May 04 '16 at 13:31
  • this is ok for when i give it for button action and i now use this for uitextfield so how i should call this controller, i am also tried to call in the method of "- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { }" by this the alert view open repeated again and again please help me.. i need to trigger it on textfield thanks. – Hari Narayanan Jul 16 '16 at 07:34
  • This is different than the OP's question, but you can use something like the `SDCAlertView` Cocoapod, which optionally lets you allow an outside tap to dismiss. – Chris Prince Apr 13 '19 at 20:32
  • Note that even though on iPad, the cancel action will not be displayed in the controller, its completion handler is still called. – Jason McClinsey Aug 26 '19 at 13:02
16

The solution which works for UIAlertController with alert style. Just needed to add gesture recognizer to alertController superview.

    [self presentViewController: alertController
                   animated: YES
                 completion:^{
                     alertController.view.superview.userInteractionEnabled = YES;
                     [alertController.view.superview addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(alertControllerBackgroundTapped)]];
                 }];

- (void)alertControllerBackgroundTapped
{
    [self dismissViewControllerAnimated: YES
                             completion: nil];
}
nominanza
  • 201
  • 2
  • 6
1

UITapGestureRecognizer didn't work for me so I've used this way:

func addDismissControl(_ toView: UIView) {
    let dismissControl = UIControl()
    dismissControl.addTarget(self, action: #selector(self.dismissAlertController), for: .touchDown)
    dismissControl.frame = toView.superview?.frame ?? CGRect.zero
    toView.superview?.insertSubview(dismissControl, belowSubview: toView)
}

func dismissAlertController() {
    self.dismiss(animated: true, completion: nil)
}

func presentAlertController(title: String?, message: String?, preferredStyle: UIAlertControllerStyle,  handler: ((UIAlertAction) -> Swift.Void)? = nil, completion: (() -> Swift.Void)? = nil) {

    let alertController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
    alertController.addAction(UIAlertAction(title: "OK", style: .default) { (alertAction) -> Void in
        handler?(alertAction)
    })

    self.present(alertController, animated: true, completion: {
        self.addDismissControl(alertController.view)
        completion?()
    })
}

func someWhereInYourViewController() {
    // ...
    presentAlertController(title: "SomeTitle", message: "SomeMessage", preferredStyle: .actionSheet, handler: { (alertAction) -> Void in
        //do some action
    }, completion: {
        //do something after presentation
    })
    // ...
}
Apoc
  • 797
  • 1
  • 10
  • 15