【问题标题】:How do I avoid unwanted extra spaces inserted by dictation into UITextField如何避免通过听写将不需要的额外空格插入 UITextField
【发布时间】:2020-03-06 14:30:43
【问题描述】:

我有一个 UITextField 用于“创建帐户”场景中的用户 ID 字段。我希望用户 ID 只包含字母数字字符,没有任何空格。

我将我的视图控制器设置为 UITextFieldDelegate 并实现了 shouldChangeCharctersIn 函数(参见下面的代码),只为字母数字返回 true。我将我的控制器设置为用户名文本字段的代表。 一切都按预期进行除非涉及复制/粘贴或听写。在这种情况下,它几乎按预期工作。如果要插入的文本包含任何或非字母数字字符,则插入成功阻止,但插入单个空格字符除外。

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

但什么都没有改变...

我在 shouldChangeCharctersIn 中添加了打印语句,以便我可以看到它何时被调用以及每次调用时返回的内容。当听写包含内部空格(例如“这是一个测试”)时,这正是在 replacementString 参数中传递给 shouldChangeCharctersIn 的内容。 shouldChangeCharctersIn 从未审查过插入以将此字符串与现有文本分开的前导空格字符。

除了将候选替换字符串记录到控制台之外,我还通过将候选字符串插入现有 UITextField 文本参数来创建结果字符串。在评估听写插入时,该空格似乎是在shouldChangeCharctersIn调用之前添加的,因为它出现在控制台输出中(例如“mikemayer67 This is a Test”) . *编辑:我在本文末尾添加了示例控制台输出。

我在这里缺少什么?

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

想法?

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 子类。它完全按照我的意愿处理键盘、听写和复制/粘贴输入。
@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
  }
}

【问题讨论】:

  • 我在发布这个问题后发现了 UITextPasteDelegate 协议。它解决了使用复制/粘贴时的问题,但没有解决听写问题。
  • 在这种情况下您希望发生什么?你想清除归档吗?你想去掉空格吗?是否要将字段重置为用户开始听写之前的内容?
  • 理想情况下,在唯一无效字符是空格的听写的情况下,将插入字符串并去除所有空格。如果听写中有非字母数字,则应阻止插入,并将字段恢复到听写尝试之前的状态。有一个帮助弹出窗口(使用信息按钮)概述了构成有效用户名的内容。我对其他建议持开放态度,但目前拒绝听写和添加空格的行为从用户体验来看是有问题的。

标签: ios uitextfield


【解决方案1】:

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

我不止一次被那个额外的空间插入烧伤了——那只是当我在其他应用程序中使用听写时……甚至没有谈论为它编写代码。

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

我将UITextField 子类化并在类内实现了所有验证和委托处理。您可以通过添加一个新的UITextField 并将其自定义类分配给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 文档

【讨论】:

  • 我曾尝试子类化和实现 dictationRecordingDidEnd,但它没有成功,因为在 dictionRecordingDidEnd 之后调用了 shouldChangeCharactersIn 并覆盖了我的更正。您的解决方案采取了解决此问题的额外步骤。我将在今晚尝试实施此修复程序,并让您知道它是如何进行的(可能通过接受这个作为正确答案。)谢谢!
猜你喜欢
  • 2021-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-02
  • 1970-01-01
  • 1970-01-01
  • 2021-06-12
  • 2018-01-27
相关资源
最近更新 更多