Reputation: 1195
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.
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.
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
Reputation: 1195
Here's what I ended up doing for anyone who has the same issue.
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
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
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