0

Swift 5, Xcode 14.2

I've got two UIViewControllers that both have to display the same UIAlertController (only one is loaded at a time). Instead of using the same code twice, I added it to my existing static class that's already used to pass data around (only accessed by one thread at a time).

In my static class:

static func createLoginDialog(buttonPressed: @escaping (_ username:String, _ pw:String) -> Void) -> UIAlertController {
    let alert = UIAlertController(title: "Login", message: "", preferredStyle: .alert)

    alert.addAction(UIAlertAction(title: "Log in", style: .default, handler: { (action: UIAlertAction!) in
        let u = alert.textFields![0].text!
        let p = alert.textFields![1].text!
        buttonPressed(u,p)
    }))
        
    alert.addTextField { textField in
        textField.placeholder = "Enter Username"
    }
    
    alert.addTextField { textField in
        textField.placeholder = "Enter Password"
    }
    
    return alert
}

Calling code in the UIViewController classes:

func doSomethingAndShowAlert() {
    //Do something

    let alert = MyStaticClass.createLoginDialog() {
        (username,pw) in
        //Do stuff with non-empty input
    }
    self.present(alert,animated: true, completion: nil)
}

With this code the dialog is dismissed once you press the button, no matter what the input is. The input is checked for validity in the calling function but I don't want to close the dialog, unless there's text in both UITextFields. According to answers to other questions, you can't keep the dialog from being dismissed automatically but you can disable the action/button until there's text, which is fine with me.

There are a couple of suggestions here (only for a single UITextField) but Xcode complains about the @objc/action: #selector if I add it to my code (because my class is static?). I also tried to combine this single TF version with this multi TF version but the action function is never called (maybe because I don't set it in viewDidLoad()?).

I'm aware that this would be a lot easier with a custom view that's displayed instead of a UIAlertController but I want to try it this way.

How do I check if there's text in both UITextFields, before enabling the button, in my static class/function?

Neph
  • 1,823
  • 2
  • 31
  • 69
  • I recommend to declare `createLoginDialog` as an **instance** method in an extension of `UIViewController`. Static code for code sharing is actually bad practice anyway. – vadian May 24 '23 at 13:46
  • You could use `addObserver(forName:object:queue:using:)` for each `UITextField`. It uses a block and not a selector, avoiding your previous issues. and allow the button once both are valid. – Larme May 24 '23 at 14:14
  • @vadian I forgot to mention, `MyStaticClass` is a struct with a `private init()`. I'd prefer to keep the function in there to have everything in a single place (not multiple extensions,...) but if I can call it the same way, I'd give it a try. What's the alternative for data 5+ ViewControllers all need access to when they're loaded? I've noticed that passing around data with segues is pretty cumbersome and it's only temporary, so `UserDefaults` isn't helpful either (plus, there are restrictions regarding types anyway). – Neph May 24 '23 at 14:19
  • @Larme In the first question I linked (in [this](https://stackoverflow.com/a/24476797/2016165) answer) people advise against using an observer with notifications because they're apparently "heavy-handed" and "unreliable", that's why I didn't test that version (plus, there's no `@IBAction` for my button anyway). Has that changed in the last 9 years? – Neph May 24 '23 at 14:25
  • I suggested the observer with a closure, not a selector/method, which can work in your case. I think it might work in your case. – Larme May 24 '23 at 14:28
  • @Larme Have observers changed since 2014? How exactly would you do it? The answer I linked is only about a single `UITextField`. – Neph May 24 '23 at 14:33
  • Quickly done: https://pastebin.com/9GrdHeM6 To rechallenge of course, like why is the method static? Who implements that method exactly? – Larme May 24 '23 at 14:57
  • If `MyStaticClass` contains only static functions, the `init` method is pointless. The benefit of the `UIViewController` extension is that it's available in any class which inherits from `UIViewController`. – vadian May 24 '23 at 15:44
  • @vadian That still leaves the question: How do I do that exactly? I already tested the multi `UITextView` version I found (and linked in the question) but it didn't work. I don't want to set anything in `viewDidLoad` (the alerts are created much later) and I also don't want to use outlets because then I could as well just create a custom VC. – Neph May 25 '23 at 08:18
  • @Larme Thanks, I'll test it later. Please also post it as an answer, links can always go offline. Everything in that class is static, so I've got access to it from whatever of the 5+ VCs (and other non-VC classes) I need it for. I use the class for e.g. bools and a bunch of lets and there are also 2 or 3 other functions. I found that's the best way to get access to the same data from multiple classes, without having to pass everything back and forth through multiple segues,.... – Neph May 25 '23 at 08:36
  • @Larme I just tested it and it works, thanks! I only had to change one thing: `loginAction.isEnabled = textFields.allSatisfy { $0.hasText }` - otherwise emptying a TF doesn't disable the button again. So far I haven't noticed any lag or problems but I still have to test it on a real device. – Neph May 25 '23 at 11:25
  • @Larme Want to post it as an answer? – Neph May 31 '23 at 08:41

1 Answers1

1

Quickly written to have the logic using your static method:

static func createLoginDialog(buttonPressed: @escaping (_ username:String, _ pw:String) -> Void) -> UIAlertController {
    let alert = UIAlertController(title: "Login", message: "", preferredStyle: .alert)
    
    let loginAction = UIAlertAction(title: "Log in", style: .default, handler: { _ in
        let u = alert.textFields![0].text!
        let p = alert.textFields![1].text!
        buttonPressed(u,p)
    })
    
    alert.addAction(loginAction)
    loginAction.isEnabled = false
    
    var textFields: [UITextField] = []
    
    alert.addTextField { textField in
        textField.placeholder = "Enter Username"
        textFields.append(textField)
        NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification,
                                               object: textField,
                                               queue: .main) { _ in
            loginAction.isEnabled = textFieldsAreValid(textFields)
        }
    }
    
    alert.addTextField { textField in
        textField.placeholder = "Enter Password"
        textFields.append(textField)
        NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification,
                                               object: textField,
                                               queue: .main) { _ in
            loginAction.isEnabled = textFieldsAreValid(textFields)
        }
    }
    
    func textFieldsAreValid(_ textFields: [UITextField]) -> Bool {
        textFields.allSatisfy { !($0.text ?? "").isEmpty } //Do the needed tests, I just check if not empty
    }
    
    return alert
}

But as Vadian said, it maybe be better to have an extension on UIViewController.

We can imagine that you have

protocol LoginProtocol: Protocol {
}

extension LoginProtocol where Self: UIViewController {
  
    func loginAlert(buttonPressed: @escaping (_ username:String, _ pw:String) -> Void) -> UIAlertController {
        //The previous code
    }

    func showLoginAlert(buttonPressed: @escaping (_ username:String, _ pw:String) -> Void) {
        let alert = loginAlert(buttonPressed: buttonPressed)
        present(alert, animated: true)
    }
}

I know that sometimes, you want to call it from classes that aren't UIViewController, but in theory, that object (be it a subview etc.), should tell its parent ViewController to show the login alert, it shouldn't do it itself. That's why I'd prefer using that way.

And on each ViewController that can show the alert:

extension MyCustomVC: LoginProtocol {}

And it can call showLoginAlert(...).

Larme
  • 24,190
  • 6
  • 51
  • 81
  • Thanks! I highly recommend using `loginAction.isEnabled = textFields.allSatisfy { $0.hasText }`, so the button is disabled again if the text in one of the `UITextField`s is deleted. – Neph Jun 01 '23 at 13:27
  • Ad the extension: The dialog is always presented by some type of `UIViewController` but it's not always created by one. I've got non-VC classes with data the currently active VC doesn't need to know about and these classes create the dialog, then pass it back to the VC. In one case the VC also isn't the one to present it but instead another `UIAlertController` that it's displayed on top of (I checked Apple guidelines, they say it's okay to display an alert on top of another, as long as you don't overdo it), so I also can't just say `present(...)`. – Neph Jun 01 '23 at 13:42
  • To shorten it you could call `textFieldsAreValid()` and set the action directly in the nested function (`func textFieldsAreValid() { loginAction.isEnabled = textFields.allSatisfy { $0.hasText } }`). ;) No need to pass the TFs (or return a `Bool`) because it's got access to the variables in its parent function. (sorry) – Neph Jun 01 '23 at 14:31