tebs1200
tebs1200

Reputation: 1195

How do you intercept a text input event from keyboard in a UITextField subclass

Background

I have created a subclass of UITextField, and I'd like to intercept any characters that the user has entered and perform some validation. Looking at the documentation, UITextField conforms to UIKeyInput and the insertText() method should be called when the user types a character on the keyboard (documentation).

Here's a very basic example:

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

}

As per the comments, insertText is never called. Conversely, deleteBackward() (which is also from UIKeyInput) gets called as expected.

Why not use UITextFieldDelegate?

The reason I'm creating the sub-class is that the control will be re-used throughout the app. It doesn't really make sense to have each ViewController that has an instance of the field re-implement the validation logic if there is a way to encapsulate it in the control.

While I might be able to get around the problem by having my subclass conform to UITextFieldDelegate, then setting delegate = self, I'd then lose the ability for any other objects to be the delegate of the field, creating a new problem.

Question

What is the best way to intercept characters from the keyboard in a subclass of UITextField?

It seems like overriding insertText() doesn't work, so is there another way to monitor text change events?

Upvotes: 0

Views: 1519

Answers (3)

tebs1200
tebs1200

Reputation: 1195

Here's what I ended up doing for anyone who has the same issue.

  • I created a private variable that stores the last valid state of the text field. That way if an update to the field fails validation, the update can be 'rejected' by reverting.
  • I had my subclass subscribe to it's own text change notifications. That way the validation can be triggered on each change (thanks for the suggestion Anton Novoselov)

Here's the code with a trivial validation example:

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
        }

    }

}

Upvotes: 1

Anton Novoselov
Anton Novoselov

Reputation: 769

Try to use this approach. Add following to your CustomTextField class. It is handler for example for EMAIL field - no allowing to enter "@" twice, etc.:

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
}


}

Upvotes: 1

Victor Alejandria
Victor Alejandria

Reputation: 120

Maybe this is going to be an overkill for what you're asking but the best way I know to do such a thing without using a delegate method is using Reactive Functional Programming, that way it's possible to listen to the events of the UITextField with an Observable object. I have some experience using ReactiveKit more specifically Bond, and with that you only need like one or two lines of code to implement what you need.

Upvotes: 0

Related Questions