如何避免通过听写插入 UITextField 中不需要的额外空格

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

我有一个 UITextField 用于 "create account" 场景中的 userid 字段。我希望 userid 只包含字母数字字符而没有任何白色 space.

我将我的视图控制器设为 UITextFieldDelegate 并实现了 shouldChangeCharctersIn 函数(参见下面的代码),仅 return 对字母数字为真。我将我的控制器设置为用户名文本字段的委托。 一切都按预期工作除非copy/paste或涉及听写。在这种情况下,它 almost 按预期工作。如果要插入的文本包含任何或非字母数字字符,插入将被成功阻止,但插入单个 space 字符除外。

一点 SO 和 Google 搜索让我明白我需要关闭 UITextField 的智能插入。所以我试着这样做。我在故事板编辑器中关闭了此字段的 SmartInsert 输入特征(见下图)。我通过在控制器的 viewDidAppear 期间检查 smartInsertDeleteType 属性 来验证这确实发生了。

但是什么都没变...

我在 shouldChangeCharctersIn 中添加了打印语句,这样我就可以看到它何时被调用以及每次调用时 returning 是什么。当听写包含内部 whitespace(例如 "This is a test")时,这正是在 replacementString 参数中传递给 shouldChangeCharctersIn[=52= 的内容]. shouldChangeCharctersIn 从未审查过为将此字符串与现有文本分开而插入的前导 space 字符。

除了将候选替换字符串记录到控制台之外,我还通过将候选字符串插入现有 UITextField 文本参数来创建结果字符串。看起来这个白色 space 被添加到 priorshouldChangeCharctersIn 的调用中,因为它在评估听写插入时出现在控制台输出中(例如 "mikemayer67 This is a Test")。 *编辑:我在此 post.

末尾添加了示例控制台输出

我在这里错过了什么?

我不想在提交表单之前简单地执行白色 space 的清理,因为这可能会让用户感到困惑,他们喜欢这种方法引入的 spaces(即使他们无法手动输入)。我也不喜欢必须弹出警报,提示他们需要更正设备造成的问题。

想法?

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
  }
}

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


编辑: 根据 DonMag 的建议,我创建了以下 UITextField 子类。它完全按照我的意愿处理键盘、听写和 copy/paste 输入。

@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
  }
}

处理听写输入可能很棘手。

我不止一次被那个额外的space插入所困扰——那是我在其他应用程序中使用听写的时候……甚至没有谈论为它编写代码。

这可能对您有用,但您可能需要进行一些调整以增强它。例如,用户完成听写后,插入点移动到字符串的末尾。

我已经 subclassed UITextField 并在 class 中实现了所有验证和委托处理。您可以简单地通过添加一个新的 UITextField 并将其自定义 class 分配给 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
    }

}

如果它不能很好地完成工作,您可能需要阅读 Apple 的 UITextInput -> Using Dictation

文档