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!

Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
jona jürgen
  • 1,789
  • 4
  • 22
  • 31

5 Answers5

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 != "")
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Full example here: https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch13p620dialogsOniPhone/ch26p888dialogsOniPhone/ViewController.swift – matt Sep 02 '14 at 16:31
  • 1
    Is there an Objective-C example of this laying around? – Adrian Sep 24 '15 at 12:04
  • Such a gorgeous, elegant answer. Thank you! I just used this on a Swift project. – Adrian Oct 15 '15 at 15:23
  • Thanks @AdrianB, you made my day. – matt Oct 15 '15 at 15:51
  • 1
    It feels like keeping the alert controller as a weak variable is more reliable than doing the nextResponder dance. Great solution overall!! – Kaan Dedeoglu Apr 15 '16 at 09:11
  • 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 function – Swinny89 Jul 22 '16 at 14:08
  • @Swinny89 I'm not picturing what you have in mind. Could you provide it as a separate answer? – matt Jul 22 '16 at 14:36
  • I have an example in Objective-C but not Swift at the moment – Swinny89 Jul 22 '16 at 14:37
  • Done, I will update my answer later on tonight with a swift example. – Swinny89 Jul 22 '16 at 14:56
  • @Swinny89 Cool, thanks! I look forward to seeing that. – matt Jul 22 '16 at 15:05
  • Current (Dec. 2016) version of the code is here: https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch13p620dialogsOniPhone/ch26p888dialogsOniPhone/ViewController.swift – matt Dec 08 '16 at 16:44
  • "Current (Dec. 2016) version of the code ..." @matt it would be nice for such a good answer to see directly here in StackOverflow the updated version of the code – Fmessina Apr 23 '18 at 15:03
  • @Fmessina The current version of the code is shown "directly here", in my answer. – matt Apr 23 '18 at 16:01
  • It is throwing Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]' – Pawan Sep 11 '18 at 13:35
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")
}
ullstrm
  • 9,812
  • 7
  • 52
  • 83
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);
}
Olie
  • 24,597
  • 18
  • 99
  • 131
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];
Swinny89
  • 7,273
  • 3
  • 32
  • 52
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)
    }
}
algrid
  • 5,600
  • 3
  • 34
  • 37
  • Thanks, this is the only version that actually works for me. How do you do this with two `UITextField`s that both have to have text in them before the "ok" button is enabled? – Neph May 24 '23 at 11:48