如何在 UITextField 子类中拦截来自键盘的文本输入事件

Posted

技术标签:

【中文标题】如何在 UITextField 子类中拦截来自键盘的文本输入事件【英文标题】:How do you intercept a text input event from keyboard in a UITextField subclass 【发布时间】:2017-05-14 09:35:41 【问题描述】:

背景

我创建了 UITextField 的子类,我想截取用户输入的任何字符并执行一些验证。查看文档,UITextField 符合UIKeyInput,并且当用户在键盘上键入字符时应调用insertText() 方法(documentation)。

这是一个非常基本的例子:

import UIKit

class CustomTextField: UITextField 

    override func insertText(_ text: String) 
        print("Character Typed: \(text)")  // never executes
        super.insertText(text)
    

    override func deleteBackward() 
        print("deleting character") // executes
        super.deleteBackward()
    


根据 cmets,insertText 永远不会被调用。相反,deleteBackward()(也来自 UIKeyInput)按预期调用。

为什么不使用 UITextFieldDelegate?

我创建子类的原因是控件将在整个应用程序中重复使用。如果有办法将其封装在控件中,那么让每个具有字段实例的 ViewController 重新实现验证逻辑真的没有意义。

虽然我可以通过让我的子类符合UITextFieldDelegate,然后设置delegate = self 来解决这个问题,但我会失去任何其他对象成为该字段代表的能力,创建一个新问题。

问题

在 UITextField 的子类中从键盘截取字符的最佳方法是什么?

似乎覆盖insertText() 不起作用,那么还有其他方法可以监控文本更改事件吗?

【问题讨论】:

【参考方案1】:

尝试使用这种方法。将以下内容添加到您的 CustomTextField 类。它是例如 EMAIL 字段的处理程序 - 不允许输入“@”两次等:

class CustomTextField: UITextField 

override func awakeFromNib() 
    self.addTarget(self, action: #selector(self.textDidchange), for: .editingChanged)
    self.delegate = self


    func textDidchange() 
//        print(self.text)
    





extension CustomTextField: UITextFieldDelegate 


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

    return handleEmailField(withRange: range, withReplacementString: string)



func handleEmailField(withRange range: NSRange, withReplacementString replacementString: String) -> Bool 
    var illegalCharactersSet = CharacterSet.init(charactersIn: "?><,\\/|`~\'\"[]±#$%^&*()=+")

    let currentString = self.text! as NSString

    let newString = currentString.replacingCharacters(in: range, with: replacementString)

    if currentString.length == 0 && replacementString == "@" 
        return false
    

    if currentString.contains("@") 
        illegalCharactersSet = CharacterSet.init(charactersIn: "?><,\\/|`~\'\"[]±#$%^&*()=+@")
    

    let components = replacementString.components(separatedBy: illegalCharactersSet)
    if components.count > 1 
        return false
    

    return newString.characters.count <= 40




【讨论】:

感谢您的建议。根据我对使用 UITextFieldDelegate 的评论,您必须使用此方法在每个使用该字段的 ViewController 中重新实现逻辑。如果可能,我想在我的子类中处理文本更改事件。 谢谢,这确实让我能够检测到事件。现在的问题是,与insertText() 不同,我无法访问更改前的文本。如果更改未通过验证,我希望将文本保持在尝试更改之前的状态。无论插入还是删除字符,这也会触发。 我可能会尝试添加一个变量来存储最后一个有效状态。如果验证失败,我将恢复到最后一个有效状态,如果它通过,我将更新最后一个有效状态。 干杯。这肯定会起作用,但现在没有其他东西可以成为您的文本字段的代表。请注意,如果您不使用委托来监控该字段,这可能不是问题。 好的,在这种情况下,我可能会建议发布通知?来自 shouldChangeCharacters() 方法。并在所有感兴趣的对象中捕获通知。【参考方案2】:

这就是我最终为遇到相同问题的任何人所做的事情。

我创建了一个私有变量来存储文本字段的最后一个有效状态。这样,如果对字段的更新验证失败,则可以通过还原来“拒绝”更新。 我让我的子类订阅了它自己的文本更改通知。这样每次更改都可以触发验证(感谢Anton Novoselov的建议)

这是一个简单的验证示例的代码:

import UIKit

class CustomTextField: UITextField 

    // Keep a copy of the last valid state so we can revert a change if it fails validation
    private var lastValidText: String?

    // Subscribe to 'editing changed' notofications from self
    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        self.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
    

    override init(frame: CGRect) 
        super.init(frame: frame)
        self.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
    

    func textDidChange() 

        let validationRegex = "^(a|e|i|o|u)+$"

        if let currentText = self.text, currentText != "" 

            if currentText.range(of: validationRegex, options: .regularExpression) != nil 

                // The update is valid - update the last valid state
                lastValidText = currentText

             else 

                // The udate failed validation - revert
                self.text = lastValidText
            

         else 

            // The field is empty. This is a valid state so reset last valid state to nil
            self.text = nil
            lastValidText = nil
        

    


【讨论】:

【参考方案3】:

也许这对你的要求来说有点过头了,但我知道在不使用委托方法的情况下做这件事的最好方法是使用响应式函数式编程,这样就可以监听带有 Observable 对象的 UITextField。我有一些使用ReactiveKit 更具体地说是Bond 的经验,因此您只需要一两行代码即可实现所需的功能。

【讨论】:

以上是关于如何在 UITextField 子类中拦截来自键盘的文本输入事件的主要内容,如果未能解决你的问题,请参考以下文章

iOS7 模拟器中的 UITextField 不接受来自 mac 键盘的任何输入

iOS 中的 UITextField 和键盘管理问题

子类化 UITextField

如何在 UITextField 子类中设置 UITextField 的属性

按下按钮和文本字段切换后 UITextField 失去焦点(键盘保持可见)

打开表情符号键盘时自定义 UITextField 冻结应用程序?