Reputation: 576
I have a UITextField for the userid field in a "create account" scenario. I want the userid to only contain alphanumeric characters without any whitespace.
I made my view controller to be a UITextFieldDelegate and implemented the shouldChangeCharctersIn function (see code below) to only return true for alphanumerics. I set my controller to be the delegate for the username text field.
Everything works as expected unless copy/paste or dictation is involved. In this case, it almost works as expected. If the text to be inserted contains any or non-alphanumeric characters, the insertion is successfully blocked except for the insertion of a single space character.
A little SO and Google searching led me to understand that I need to turn off smart insertion for the UITextField. So I tried to do that. I turned off the SmartInsert input trait (see image below) for this field in the storyboard editor. I verified that this actually took by checking the smartInsertDeleteType property during the controller's viewDidAppear.
But nothing changed...
I added print statements into shouldChangeCharctersIn so that I could see when it is being invoked and what it is returning on each invocation. When dictation contains internal whitespace (e.g. "This is a test"), that is exactly what is passed in the replacementString parameter to shouldChangeCharctersIn. The leading space character that was inserted to separate this string from the existing text was never vetted by shouldChangeCharctersIn.
In addition to logging the candidate replacement string to the console, I created the resultant string from inserting the candidate string into the existing UITextField text parameter. It appears that this white space was added prior to the call to shouldChangeCharctersIn as it appears in the console output when evaluating the dictation insertion (e.g. "mikemayer67 This is a Test"). *Edit: I added sample console output at the end of this post.
What am I missing here?
I don't want to simply perform a cleanup of whitespace before submitting the form as this may lead to a confused user who likes the spaces introduced by this method (even though they cannot enter them manually). I don't like either the idea of having to pop up an alert that they need to correct a problem that was created by the device.
Thoughts?
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
@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
}
}
Upvotes: 0
Views: 502
Reputation: 77690
Dealing with dictation input can be tricky.
I've been burned more than once by that extra-space insertion - and that's just when I'm using dictation in other apps... not even talking about writing code for it.
This might work for you, although you may want to do some tweaking to enhance it. For example, after the user finishes dictation, the insertion point moves to the end of the string.
I've subclassed UITextField
and implemented all the validation and delegate handling inside the class. You can try it out simply by adding a new UITextField
and assigning its custom class to 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
}
}
If it doesn't quite do the job, you may want to take a read through Apple's docs for UITextInput -> Using Dictation
Upvotes: 1