Prevent dismissal of UIAlertController
Asked Answered
P

5

54

I am adding a UITextField to a UIAlertController, which appears as an AlertView. Before dismissing the UIAlertController, I want to validate the input of the UITextField. Based on the validation I want to dismiss the UIAlertController or not. But I have no clue how to prevent the dismissing action of the UIAlertController when a button is pressed. Has anyone solved this problem or any ideas where to start ? I went to google but no luck :/ Thanks!

Plugugly answered 2/9, 2014 at 16:21 Comment(0)
C
80

You're correct: if the user can tap a button in your alert, the alert will be dismissed. So you want to prevent the user from tapping the button! It's all just a matter of disabling your UIAlertAction buttons. If an alert action is disabled, the user can't tap it to dismiss.

To combine this with text field validation, use a text field delegate method or action method (configured in the text field's configuration handler when you create it) to enable/disable the UIAlertActions appropriately depending on what text has (or hasn't) been entered.

Here's an example. We created the text field like this:

alert.addTextFieldWithConfigurationHandler {
    (tf:UITextField!) in
    tf.addTarget(self, action: "textChanged:", forControlEvents: .EditingChanged)
}

We have a Cancel action and an OK action, and we brought the OK action into the world disabled:

(alert.actions[1] as UIAlertAction).enabled = false

Subsequently, the user can't tap OK unless there is some actual text in the text field:

func textChanged(sender:AnyObject) {
    let tf = sender as UITextField
    var resp : UIResponder = tf
    while !(resp is UIAlertController) { resp = resp.nextResponder() }
    let alert = resp as UIAlertController
    (alert.actions[1] as UIAlertAction).enabled = (tf.text != "")
}

EDIT Here's the current (Swift 3.0.1 and later) version of the above code:

alert.addTextField { tf in
    tf.addTarget(self, action: #selector(self.textChanged), for: .editingChanged)
}

and

alert.actions[1].isEnabled = false

and

@objc func textChanged(_ sender: Any) {
    let tf = sender as! UITextField
    var resp : UIResponder! = tf
    while !(resp is UIAlertController) { resp = resp.next }
    let alert = resp as! UIAlertController
    alert.actions[1].isEnabled = (tf.text != "")
}
Celloidin answered 2/9, 2014 at 16:25 Comment(14)
Full example here: github.com/mattneub/Programming-iOS-Book-Examples/blob/master/…Celloidin
Is there an Objective-C example of this laying around?Prove
Such a gorgeous, elegant answer. Thank you! I just used this on a Swift project.Prove
Thanks @AdrianB, you made my day.Celloidin
It feels like keeping the alert controller as a weak variable is more reliable than doing the nextResponder dance. Great solution overall!!Calamint
To add to this, you can create a closure and set that to be the target and set the selector to "invoke". That way you can keep it all in the same functionTorrens
@Torrens I'm not picturing what you have in mind. Could you provide it as a separate answer?Celloidin
I have an example in Objective-C but not Swift at the momentTorrens
Done, I will update my answer later on tonight with a swift example.Torrens
@Torrens Cool, thanks! I look forward to seeing that.Celloidin
Current (Dec. 2016) version of the code is here: github.com/mattneub/Programming-iOS-Book-Examples/blob/master/…Celloidin
"Current (Dec. 2016) version of the code ..." @Celloidin it would be nice for such a good answer to see directly here in StackOverflow the updated version of the codePaillasse
@Paillasse The current version of the code is shown "directly here", in my answer.Celloidin
It is throwing Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'Corcoran
P
21

I've simplified matt's answer without the view hierarcy traversing. This is holding the action itself as a weak variable instead. This is a fully working example:

weak var actionToEnable : UIAlertAction?

func showAlert()
{
    let titleStr = "title"
    let messageStr = "message"

    let alert = UIAlertController(title: titleStr, message: messageStr, preferredStyle: UIAlertControllerStyle.Alert)

    let placeholderStr =  "placeholder"

    alert.addTextFieldWithConfigurationHandler({(textField: UITextField) in
        textField.placeholder = placeholderStr
        textField.addTarget(self, action: "textChanged:", forControlEvents: .EditingChanged)
    })

    let cancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: { (_) -> Void in

    })

    let action = UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: { (_) -> Void in
        let textfield = alert.textFields!.first!

        //Do what you want with the textfield!
    })

    alert.addAction(cancel)
    alert.addAction(action)

    self.actionToEnable = action
    action.enabled = false
    self.presentViewController(alert, animated: true, completion: nil)
}

func textChanged(sender:UITextField) {
    self.actionToEnable?.enabled = (sender.text! == "Validation")
}
Proportionate answered 19/3, 2016 at 8:58 Comment(1)
@ullstrm: Thanks it helped a lot.Corcoran
J
7

Cribbing off of @Matt's answer, here's how I did the same thing in Obj-C

- (BOOL)textField: (UITextField*) textField shouldChangeCharactersInRange: (NSRange) range replacementString: (NSString*)string
{
    NSString *newString = [textField.text stringByReplacingCharactersInRange: range withString: string];

    // check string length
    NSInteger newLength = [newString length];
    BOOL okToChange = (newLength <= 16);    // don't allow names longer than this

    if (okToChange)
    {
        // Find our Ok button
        UIResponder *responder = textField;
        Class uiacClass = [UIAlertController class];
        while (![responder isKindOfClass: uiacClass])
        {
            responder = [responder nextResponder];
        }
        UIAlertController *alert = (UIAlertController*) responder;
        UIAlertAction *okAction  = [alert.actions objectAtIndex: 0];

        // Dis/enable Ok button based on same-name
        BOOL duplicateName = NO;
        // <check for duplicates, here>

        okAction.enabled = !duplicateName;
    }


    return (okToChange);
}
Josefjosefa answered 3/11, 2015 at 1:2 Comment(0)
T
5

I realise that this is in Objectiv-C but it shows the principal. I will update this with a swift version later.

You could also do the same using a block as the target.

Add a property to your ViewController so that the block (closure for swift) has a strong reference

@property (strong, nonatomic) id textValidationBlock;

Then create the AlertViewController like so:

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Title" message:@"Message" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {

}];

   __weak typeof(self) weakSelf = self;
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [weakSelf doSomething];

}];
[alertController addAction:cancelAction];
[alertController addAction:okAction];
[alertController.actions lastObject].enabled = NO;
self.textValidationBlock = [^{
    UITextField *textField = [alertController.textFields firstObject];
    if (something) {
        alertController.message = @"Warning message";
        [alertController.actions lastObject].enabled = NO;
    } else if (somethingElse) {
        alertController.message = @"Another warning message";
        [alertController.actions lastObject].enabled = NO;
    } else {
        //Validation passed
        alertController.message = @"";
        [alertController.actions lastObject].enabled = YES;
    }

} copy];
[alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
    textField.placeholder = @"placeholder here";
    [textField addTarget:weakSelf.textValidationBlock action:@selector(invoke) forControlEvents:UIControlEventEditingChanged];
}];
[self presentViewController:alertController animated:YES completion:nil];
Torrens answered 22/7, 2016 at 14:42 Comment(0)
C
0

Here's the same idea as in other answers, but I wanted a simple method isolated in an extension and available for use in any UIViewController subclass. It shows an alert with one text input field and two buttons: ok and cancel.

extension UIViewController {

    func askForTextAndConfirmWithAlert(title: String, placeholder: String, okHandler: @escaping (String?)->Void) {
        
        let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
        
        let textChangeHandler = TextFieldTextChangeHandler { text in
            alertController.actions.first?.isEnabled = !(text ?? "").isEmpty
        }
        
        var textHandlerKey = 0
        objc_setAssociatedObject(self, &textHandlerKey, textChangeHandler, .OBJC_ASSOCIATION_RETAIN)

        alertController.addTextField { textField in
            textField.placeholder = placeholder
            textField.clearButtonMode = .whileEditing
            textField.borderStyle = .none
            textField.addTarget(textChangeHandler, action: #selector(TextFieldTextChangeHandler.onTextChanged(sender:)), for: .editingChanged)
        }

        let okAction = UIAlertAction(title: CommonLocStr.ok, style: .default, handler: { _ in
            guard let text = alertController.textFields?.first?.text else {
                return
            }
            okHandler(text)
            objc_setAssociatedObject(self, &textHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN)
        })
        okAction.isEnabled = false
        alertController.addAction(okAction)

        alertController.addAction(UIAlertAction(title: CommonLocStr.cancel, style: .cancel, handler: { _ in
            objc_setAssociatedObject(self, &textHandlerKey, nil, .OBJC_ASSOCIATION_RETAIN)
        }))

        present(alertController, animated: true, completion: nil)
    }

}

class TextFieldTextChangeHandler {
    
    let handler: (String?)->Void
    
    init(handler: @escaping (String?)->Void) {
        self.handler = handler
    }

    @objc func onTextChanged(sender: AnyObject) {
        handler((sender as? UITextField)?.text)
    }
}
Corenda answered 23/11, 2020 at 18:28 Comment(1)
Thanks, this is the only version that actually works for me. How do you do this with two UITextFields that both have to have text in them before the "ok" button is enabled?Frug

© 2022 - 2024 — McMap. All rights reserved.