'iOS keyboard: How can I start with a disabled done/return/go button on the keyboard and KEEP it disabled until the validation requirements are met?

I've got a text field delegate setup which disables/enables the done button on keyboard when certain conditions are met (between 5 and 15 characters, not just whitespace, etc...) using the "inputDelegate.returnKeyEnabled" key (which is wrapped by ElId.Key.enableDone). Everything seemed to work fine, except the done button would start off enabled before editing.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    
    guard let text = textField.text else { return true }
                    
    let newLength = text.count + string.count - range.length
    let booleanValue = isValidText(text, length: newLength)
    
    self.rightActionButton?.isEnabled = booleanValue
    textField.enablesReturnKeyAutomatically = false
    textField.setValue(booleanValue,
                       forKeyPath: ElId.Key.enableDone)

    return true
}

func isValidText(_ text: String, length: Int) -> Bool {
    return ((length >= minNameLength) && (length <= maxNameLength)) && !text.trimmingCharacters(in: .whitespaces).isEmpty && !text.last!.isWhitespace && !text.first!.isWhitespace
}

At first I tried to set the textfield's "inputDelegate.returnKeyEnabled" false beforehand, but that didn't work.

After reading other answers here, it looked like I could start with the done button by setting enablesReturnKeyAutomatically to true before editing. And it sort of did; except: The done button enables after I start typing, until I've tapped a certain number of characters and then start deleting. At this point, it starts to work as expected.

func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
    textField.enablesReturnKeyAutomatically = true
//    textField.setValue(false,
//                       forKeyPath: ElId.Key.enableDone)
    
    return true
}

I've confirmed through breakpoints that enablesReturnKeyAutomatically is only true at first and then is false after the first keystroke and that the isValidText method is returning false from the first keystroke; but done button enables as soon as I start typing and then stays enabled until after several keystrokes.

How can I start with a disabled done/return/go button on the keyboard and KEEP it disabled until the validation requirements are met?

UPDATE

Realized that while the length was validating properly, my whitespace checks were still being applied to the previous value not the new one. Thought this might be the culprit and updated my method to correct for this, however now the validation not working at all. isValidText method still returns false when it should, but done button is always enabled. Maybe this will draw light onto the mistake.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    
    guard let text = textField.text,
            let textRange = Range(range, in: text) else { return true }
                    
    let updatedText = text.replacingCharacters(in: textRange,
                                               with: string)
    
//    let newLength = text.count + string.count - range.length
    let booleanValue = isValidText(updatedText)//, length: newLength)
    
    self.rightActionButton?.isEnabled = booleanValue
    textField.enablesReturnKeyAutomatically = false
    textField.setValue(booleanValue,
                       forKeyPath: ElId.Key.enableDone)

    return true
}

func isValidText(_ text: String/*, length: Int*/) -> Bool {
    return ((text.count >= minNameLength) && (text.count <= maxNameLength)) && !text.trimmingCharacters(in: .whitespaces).isEmpty && !text.last!.isWhitespace && !text.first!.isWhitespace
}

When I stop setting enablesReturnKeyAutomatically to true beforehand, the new version of the method works as expected. The only problem is that the Done button is then enabled from the beginning but immediately disables as text is written.



Solution 1:[1]

Actually disabling the return key seems to be difficult. Source: This discussion How to disable/enable the return key in a UITextField?

But, since you're using the 'text selection change' method instead of the 'should return' method, I would guess that maybe a solution that just prevents the user submitting invalid input would work for you?

If so, here is a workaround. It doesn't actually disable the return key, but it does disable the 'NEXT' button under the exact conditions you described in your question. The button starts out disabled, and after every key press, checks again whether to enable/disable the button based on your isValidText function.

Here is a minimal example so you can test whether this works for you. In order to recreate the project, all you'll have to do is set up the text field, label, and button in Main.storyboard, copy and paste this into ViewController, then connect the @IBOutlets and @IBAction:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var testTextField: UITextField!
    @IBOutlet weak var resultLabel: UILabel!
    @IBOutlet weak var nextButton: UIButton!
    
    let maxNameLength = 15
    let minNameLength = 5
    
    override func viewDidLoad() {
        super.viewDidLoad()
        testTextField.delegate = self
        // Start off the button as disabled, as the button should only be
        // enabled under the conditions that valid text has been entered
        nextButton.isEnabled = false
    }
    
    @IBAction func doNextStep(_ sender: UIButton) {
        print("On to Next Step")
    }
}

extension ViewController: UITextFieldDelegate {
    func isValidText(_ text: String/*, length: Int*/) -> Bool {
        return ((text.count >= minNameLength) && (text.count <= maxNameLength)) && !text.trimmingCharacters(in: .whitespaces).isEmpty && !text.last!.isWhitespace && !text.first!.isWhitespace
    }
    
    func textFieldDidChangeSelection(_ textField: UITextField) {
        if let txt = textField.text {
            let result = isValidText(txt)
            resultLabel.text = result ? "Valid Text" : "Not Valid"
            nextButton.isEnabled = result
        } else {
            nextButton.isEnabled = false
        }
    }
}

It isn't actually necessary to use the shouldChangeCharactersInRange method: you can get the same effect with textFieldDidChangeSelection since you're checking whether the text is valid in order to proceed/not proceed with some other action, and not to decide whether to allow the text inside the text field to be changed (unless I misunderstood your question).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Quack E. Duck