简体   繁体   中英

How do I avoid unwanted extra spaces inserted by dictation into UITextField

I have a UITextField for the userid field in a "create account" scenario. I want the userid to only contain alphanumeric characters without any whitespace.

I made my view controller to be a UITextFieldDelegate and implemented the shouldChangeCharctersIn function (see code below) to only return true for alphanumerics. I set my controller to be the delegate for the username text field. Everything works as expected unless copy/paste or dictation is involved. In this case, it almost works as expected. If the text to be inserted contains any or non-alphanumeric characters, the insertion is successfully blocked except for the insertion of a single space character.

A little SO and Google searching led me to understand that I need to turn off smart insertion for the UITextField. So I tried to do that. I turned off the SmartInsert input trait (see image below) for this field in the storyboard editor. I verified that this actually took by checking the smartInsertDeleteType property during the controller's viewDidAppear .

But nothing changed...

I added print statements into shouldChangeCharctersIn so that I could see when it is being invoked and what it is returning on each invocation. When dictation contains internal whitespace (eg "This is a test"), that is exactly what is passed in the replacementString parameter to shouldChangeCharctersIn . The leading space character that was inserted to separate this string from the existing text was never vetted by shouldChangeCharctersIn .

In addition to logging the candidate replacement string to the console, I created the resultant string from inserting the candidate string into the existing UITextField text parameter. It appears that this white space was added prior to the call to shouldChangeCharctersIn as it appears in the console output when evaluating the dictation insertion (eg "mikemayer67 This is a Test"). *Edit: I added sample console output at the end of this post.

What am I missing here?

I don't want to simply perform a cleanup of whitespace before submitting the form as this may lead to a confused user who likes the spaces introduced by this method (even though they cannot enter them manually). I don't like either the idea of having to pop up an alert that they need to correct a problem that was created by the device.

Thoughts?

extension CreateAccountController : UITextFieldDelegate
{
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
  {
    guard let value = textField.text else { return false }
    let testString = (value as NSString).replacingCharacters(in: range, with: string)

    let rval = validate(textField,string:string)
    print("allow: '\(string)' '\(testString)' ", (rval ? "OK" : "NOPE"))
    return rval
  }

  func validate(_ textField: UITextField, string:String) -> Bool
  {
    var allowedCharacters = CharacterSet.alphanumerics
    if textField == password1TextField || textField == password2TextField
    {
      allowedCharacters.insert(charactersIn: "-!:#$@.")
    }

    return string.rangeOfCharacter(from: allowedCharacters.inverted) == nil
  }
}

用户名 UITextField 输入设置

allow: 'm' 'm'  OK
allow: 'i' 'mi'  OK
allow: 'k' 'mik'  OK
allow: 'e' 'mike'  OK
allow: ' ' 'mike '  NOPE
allow: 'm' 'mikem'  OK
allow: 'a' 'mikema'  OK
allow: 'y' 'mikemay'  OK
allow: 'e' 'mikemaye'  OK
allow: 'r' 'mikemayer'  OK
allow: 'this is a test ' 'mike this is a test mayer'  NOPE


Edit: Based on the suggestion by DonMag, I created the following UITextField subclass. It handles keyboard, dictation, and copy/paste entry exactly as I would like.

 @IBDesignable class LoginTextField: UITextField, UITextFieldDelegate { @IBInspectable var allowPasswordCharacters : Bool = false var validatedText: String? var dictationText: String? override init(frame: CGRect) { super.init(frame: frame) delegate = self } required init?(coder: NSCoder) { super.init(coder: coder) delegate = self } // editing started, so save current text func textFieldDidBeginEditing(_ textField: UITextField) { validatedText = text dictationText = nil } // When dictation ends, the text property will be what we *expect* // to show up if *shouldChangeCharactersIn* returns true // Validate the dictated string and either cache it or reset it to // the last validated text override func dictationRecordingDidEnd() { dictationText = nil if let t = text { let stripped = t.replacingOccurrences(of: " ", with: "") if validate(string:stripped) { dictationText = stripped } else { dictationText = validatedText } } } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let t = dictationText { // Handle change here, don't let UIKit do it text = t validatedText = t dictationText = nil } else if let value = textField.text { let testString = (value as NSString).replacingCharacters(in: range, with: string).replacingOccurrences(of: " ", with: "") if validate(string:testString) { text = testString validatedText = testString } } return false } func validate(string:String) -> Bool { var allowedCharacters = CharacterSet.alphanumerics if allowPasswordCharacters { allowedCharacters.insert(charactersIn: "-!:#$@.") } return string.rangeOfCharacter(from: allowedCharacters.inverted) == nil } }

Dealing with dictation input can be tricky.

I've been burned more than once by that extra-space insertion - and that's just when I'm using dictation in other apps... not even talking about writing code for it.

This might work for you, although you may want to do some tweaking to enhance it. For example, after the user finishes dictation, the insertion point moves to the end of the string.

I've subclassed UITextField and implemented all the validation and delegate handling inside the class. You can try it out simply by adding a new UITextField and assigning its custom class to MyTextField :

class MyTextField: UITextField, UITextFieldDelegate {

    var myCurText: String?
    var myNewText: String?

    var isDictation: Bool = false

    override init(frame: CGRect) {
        super.init(frame: frame)
        delegate = self
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        delegate = self
    }

    // editing started, so save current text
    func textFieldDidBeginEditing(_ textField: UITextField) {
        // unwrap the text
        if let t = text {
            myCurText = t
        }
    }

    // when dictation ends, the text will be what we *expect*
    //  e.g.
    //      text is "ABCD"
    //      insertion point is between the B and C
    //      user dictates "Test"
    //      text is now "ABTestCD"
    //  or
    //      user dictates "This is a test"
    //      text is now "ABThis is a testCD"
    //
    // So, we can validate the string and set a flag telling
    //  shouldChangeCharactersIn range not to do normal processing
    override func dictationRecordingDidEnd() {
        // set flag that we just dictated something
        isDictation = true

        // unwrap the text
        if let t = text {
            // just for debuggging
            print("Dictation Ended: [\(t)]")
            // strip spaces from the whole string
            let stripped = t.replacingOccurrences(of: " ", with: "")
            // validate the stripped string
            if validate(self, string: stripped) {
                // just for debugging
                print("Valid! setting text to:", stripped)
                // it's a valid string, so update myNewText
                myNewText = stripped
            } else {
                // just for debugging
                print("NOT setting text to:", stripped)
                // it's NOT a valid string, so set myNewText to myCurText
                myNewText = myCurText
            }
        }
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

        // if we just received a dictation
        if isDictation {
            // update self.text
            text = myNewText
            // update myCurText variable
            myCurText = myNewText
            // turn off the dictation flag
            isDictation = false
            // returning false from shouldChangeCharactersIn
            return false
        }

        // we get here if it was NOT a result of dictation

        guard let value = textField.text else { return false }
        let testString = (value as NSString).replacingCharacters(in: range, with: string)

        let rval = validate(textField,string:string)
        print("allow: '\(string)' '\(testString)' ", (rval ? "OK" : "NOPE"))

        if rval {
            // if valid string, update myCurText variable
            myCurText = testString
        }
        return rval

    }

    func validate(_ textField: UITextField, string:String) -> Bool
    {
        var allowedCharacters = CharacterSet.alphanumerics
        allowedCharacters.insert(charactersIn: "-!:#$@.")
        return string.rangeOfCharacter(from: allowedCharacters.inverted) == nil
    }

}

If it doesn't quite do the job, you may want to take a read through Apple's docs for UITextInput -> Using Dictation

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM