I have four UITextFields, each of which represents a single digit of an OTP. I want the control to shift to consecutive textfield as the user types in the code. My implementation is below.
extension FillSignUpCodeController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.text = ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let inputString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if inputString.count == 1 {
switch textField {
case textFieldCodeFirstDigit:
textFieldCodeFirstDigit.text = inputString
textFieldCodeFirstDigit.resignFirstResponder()
textFieldCodeSecondDigit.becomeFirstResponder()
case textFieldCodeSecondDigit:
textFieldCodeSecondDigit.text = inputString
textFieldCodeSecondDigit.resignFirstResponder()
textFieldCodeThirdDigit.becomeFirstResponder()
case textFieldCodeThirdDigit:
textFieldCodeThirdDigit.text = inputString
textFieldCodeThirdDigit.resignFirstResponder()
textFieldCodeFourthDigit.becomeFirstResponder()
case textFieldCodeFourthDigit:
textFieldCodeFourthDigit.text = inputString
textFieldCodeFourthDigit.resignFirstResponder()
default:
return false
}
}
return true
}
}
With this piece of code, as the user types the first digit, the first textfield takes the input value and moves the control to the next textfield. However, the second text field is taking the value of the first digit. I tried setting the text to empty after changing the firstResponder but it did not work. How can I fix this issue? Thanks.
Since textField(_:shouldChangeCharactersIn:replacementString:) executes before the text is present in the text field, you should put the code inside a method that is called when the text did already change.
You should observe the changes of your text field and execute the responder-changing code there. You can find more information about that in this question.
Moreover, resigning the first responder before changing it is redundant, you don't need to do that.
It is also very redundant to handle every text field separately. I'd recommend including your text fields in an array and iterating through them.
shouldChangeCharactersInRange gets called before the textField is filled. You can do it by:
textField.addTarget(self, action: #selector(textFieldChangedValue(textField:)), for: UIControlEvents.editingChanged)
write above line for all textfields in viewDidLoad()
#objc func textFieldChangedValue(textField: UITextField) {
print(textField.text)
}
This will work
Following the answers from #the4kman and #Rahul Dasgupta, I have implemented the following:
FillUpCodeViewController.swift
override func viewDidLoad() {
setupArrrayofTextFields(textField1: textFieldCodeFirstDigit,
textField2: textFieldCodeSecondDigit,
textField3: textFieldCodeThirdDigit,
textField4: textFieldCodeFourthDigit)
}
func setupArrrayofTextFields(textField1: UITextField, textField2: UITextField,
textField3: UITextField, textField4: UITextField) {
arrayOftextFields = [textField1, textField2, textField3, textField4]
for textField in arrayOftextFields {
textField.addTarget(self, action: #selector(textFieldDidChange(textField:)), for: .editingChanged)
}
}
#objc func textFieldDidChange(textField: UITextField) {
guard textField.text?.count == 0 else {
let index: Int = arrayOftextFields.index(of: textField)!
guard index == (arrayOftextFields.count-1) else {
arrayOftextFields[index+1].becomeFirstResponder()
return
}
textField.resignFirstResponder()
return
}
}
And, again in the viewcontroller in which I have to implement the submission of recovery code, I simply inherited the FillUpCodeViewController class.
RecoveryViewController.swift
override func viewDidLoad() {
setupArrrayofTextFields(textField1: textFieldRecoveyCodeFirstDigit,
textField2: textFieldRecoveyCodeSecondDigit,
textField3: textFieldRecoveyCodeThirdDigit,
textField4: textFieldRecoveyCodeFourthDigit)
}
Related
I want to enable and change color of a button depending on text in password and confirm password textfields. So I found this code online:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let tfConfirmPasswordText = (tfConfirmPassword.text! as NSString).replacingCharacters(in: range, with: string)
if tfConfirmPasswordText.isEmpty {
btnContinue.backgroundColor = UIColor(named: "labelColor")
btnContinue.isEnabled = false
}
else if tfPassword.text != "" && !tfConfirmPasswordText.isEmpty {
btnContinue.backgroundColor = UIColor(named: "accentColor")
btnContinue.isEnabled = true
}
return true
}
This code does work and using these conditions it will enable or disable continue button according to confirmPassword textfield.
But the problem is that after filling both password and confirmPassword textfields, if I remove text from password field it still keeps the button enabled and only disables it if text is removed from confirmPassword textfield.
And if I put password.delegate = self alongside confirmPassword.delgate = self in viewDidLoafd(), it crashes when I put more than 1 character in any textfield.
Is there any way to enable or disable button by always keeping a check on both the textfields instead of just one..ie. confirmPassword textfield?
Instead of using shouldChangeCharactersIn ,i used like below to make it works
override func viewDidLoad() {
super.viewDidLoad()
//initially at disabled
disabledButton()
[tfPassword, tfConfirmPasswordText].forEach({ $0.addTarget(self, action: #selector(textFieldEditingDidChange), for: .editingChanged) })
}
#objc func textFieldEditingDidChange(sender: UITextField) {
guard
let tfPasswordTxtValue = tfPassword.text, !tfPasswordTxtValue.isEmpty,
let tfConfirmPasswordTextValue = tfConfirmPasswordText.text, !tfConfirmPasswordTextValue.isEmpty
else {
disabledButton()
return
}
enableButton()
}
func disabledButton(){
btnContinue.backgroundColor = UIColor(named: "labelColor")
btnContinue.isEnabled = false
}
func enableButton(){
btnContinue.isEnabled = true
}
I think you're missing a test:
Instead of
if tfConfirmPasswordText.isEmpty
I would have write
if tfConfirmPasswordText.isEmpty || tfpasswordText.isEmpty
I am building a digit only textfield.
I would like that when the text in textfield is "" replace it with "0".
I only manage to get the current text in read only.
Thanks in advance
Add the editingChanged delegate method to textField as follows:
In the viewDidLoad() method add the following code:
textField.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged);
And then, implement the method as follows:
#objc func textFieldChanged(_ textField: UITextField) {
if let text = textField.text, text.isEmpty {
textField.text = "0"
}
}
The above method will be called every time the textField content changes.
You can also set the initial text of the textField to "0". Simply use textField.text = "0" in the viewDidLoad() method.
If you want the text field to update when you have finished editing you can set up the action like this
and then
#IBAction func editingDidEnd(_ txtTest: UITextField) {
if txtTest.text == ""
{
txtTest.text = "0"
}
textField.addTarget(self, action: #selector(handleTextField(_:)), for: .editingChanged)
#objc private func handleTextField(_ textField: UITextField) {
if textField.text == "" {
textFiled.text = "0"
}
}
I am trying to create otp textfield using five textfield.All working fine if you add top, but issue is occurred when user try to add textfield empty and trying to backspace and it was not call any delegate method of UItextfiled which I already added.
I tried this :-
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let char = string.cStringUsingEncoding(NSUTF8StringEncoding)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
println("Backspace was pressed")
}
return true
}
but it's called when textfield is not empty.
For example :-
In below screen shot add 1 and on two different textfield and third one is empty but when I try to backspace it's need to go in second textfield(third is field is empty) this is what I was facing issue from mine side.
Thanks
followed by #Marmik Shah and #Prashant Tukadiya answer here I add my answer , for quick answer I taken the some code from here
step 1 :
create the IBOutletCollection for your all textfields as well as don't forget to set the tag in all textfields in the sequence order, for e.g [1,2,3,4,5,6]
class ViewController: UIViewController{
#IBOutlet var OTPTxtFields: [MyTextField]! // as well as set the tag for textfield in the sequence order
override func viewDidLoad() {
super.viewDidLoad()
//change button color and other options
OTPTxtFields.forEach { $0.textColor = .red; $0.backspaceTextFieldDelegate = self }
OTPTxtFields.first.becomeFirstResponder()
}
step 2 :
in your current page UITextField delegate method
extension ViewController : UITextFieldDelegate, MyTextFieldDelegate {
func textFieldDidEnterBackspace(_ textField: MyTextField) {
guard let index = OTPTxtFields.index(of: textField) else {
return
}
if index > 0 {
OTPTxtFields[index - 1].becomeFirstResponder()
} else {
view.endEditing(true)
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let newString = ((textField.text)! as NSString).replacingCharacters(in: range, with: string)
if newString.count < 2 && !newString.isEmpty {
textFieldShouldReturnSingle(textField, newString : newString)
// return false
}
return newString.count < 2 || string == ""
//return true
}
override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(copy(_:)) || action == #selector(paste(_:)) {
return false
}
return true
}
func textFieldShouldReturnSingle(_ textField: UITextField, newString : String)
{
let nextTag: Int = textField.tag + 1
textField.text = newString
let nextResponder: UIResponder? = textField.superview?.viewWithTag(nextTag)
if let nextR = nextResponder
{
// Found next responder, so set it.
nextR.becomeFirstResponder()
}
else
{
// Not found, so remove keyboard.
textField.resignFirstResponder()
callOTPValidate()
}
}
}
Step 3:
create the textfield class for access the backward function
class MyTextField: UITextField {
weak var myTextFieldDelegate: MyTextFieldDelegate?
override func deleteBackward() {
if text?.isEmpty ?? false {
myTextFieldDelegate?.textFieldDidEnterBackspace(self)
}
super.deleteBackward()
}
}
protocol MyTextFieldDelegate: class {
func textFieldDidEnterBackspace(_ textField: MyTextField)
}
step - 4
finally follow the #Marmik Shah answer for custom class for your UITextField
Step 5
get the values from each textfield use this
func callOTPValidate(){
var texts: [String] = []
OTPTxtFields.forEach { texts.append($0.text!)}
sentOTPOption(currentText: texts.reduce("", +))
}
func sentOTPOption(currentText: String) {
print("AllTextfieldValue == \(currentText)")
}
You can override the function deleteBackward()
Create a new Class that inherits UITextField and trigger and EditingEnd event.
class MyTextField: UITextField {
override func deleteBackward() {
super.deleteBackward()
print("Backspace");
self.endEditing(true);
}
}
Then, in your Storyboard, add a custom class for the UITextField
Next, in your view controller, in the editingEnd action, you can switch the textfield. For this to work, you will need to set a tag value for each of your textfield.
For example, you have two text fields, tfOne and tfTwo.
tfOne.tag = 1; tfTwo.tag = 2
Now, if currently you are editing tfTwo and backspace is clicked, then you set the currently editing text field to tfOne
class ViewController: UIViewController {
#IBOutlet weak var tfOne: MyTextField!
#IBOutlet weak var tfTwo: MyTextField!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func editingEnded(_ sender: UITextField) {
// UITextField editing ended
if(sender.tag == 2) {
self.tfOne.becomeFirstResponder();
}
}
}
You can give tag to your textfield in sequence like 101,102,103,104,105.
when backspace tapped. check the length of string is equal to 0. then goto textfield.tag - 1 until you get first textfield.like if you are on textfield 102 then goto textfield 102-1 = 101.
Same as when enter any character goto next textfield until you reach to last textfield like if you are on textfield 102 then goto textfield 102+1 = 103.
You can use (self.view.viewWithTag(yourTag) as? UITextField)?.becomeFirstResponder()
I don't have system with me so couldn't able to post code
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?
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
}
}
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
}
}
}
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.
I have login controller where it has two textFields:
Access Card
Password
The Max Length for the access card is 9 and once the user type the ninth number, it should appear on the access card filed then the cursor needs to move to the password field.
In my code, the cursor is moving when the user clicks to enter the ninth number but the number doesn't appear and the cursor moves to the password field.
For example: I want to enter "123456789" as access card. Once I click "9" it doesn't appear but the cursor moves to password field:
LoginController.swift:
let ACCESSCARD_MAXLENGTH = 9
let PASSWORD_MAXLENGTH = 12
var AccessCardtextFieldLength = 0
var PasswordTextFieldLength = 0
class LoginViewController: UIViewController , UITextFieldDelegate {
#IBOutlet weak var AccessCardTextField: UITextField!
#IBOutlet weak var PasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// calling the function that initialize textFields
initializeTextFields()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// function is used to initialize textFields
func initializeTextFields () {
// To set the focus on the access card once the view load.
AccessCardTextField.becomeFirstResponder()
// This must be defined so we can apply the text field functions on it
AccessCardTextField.delegate = self
PasswordTextField.delegate = self
// Define the keyboard type of the textFields.
AccessCardTextField.keyboardType = UIKeyboardType.NumberPad
PasswordTextField.keyboardType = UIKeyboardType.ASCIICapable
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
AccessCardtextFieldLength = (textField.text?.characters.count)! + string.characters.count
PasswordTextFieldLength = (textField.text?.characters.count)! + string.characters.count
if (textField == AccessCardTextField){
for i in 0..<ACCESSCARD_MAXLENGTH{
if (AccessCardtextFieldLength == ACCESSCARD_MAXLENGTH){
PasswordTextField.becomeFirstResponder()
}
else{
return true
}
return false
}
}
if (textField == PasswordTextField){
return PasswordTextFieldLength <= PASSWORD_MAXLENGTH ? true : false
}
return true
}
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool will only update when it return true. In this case you are changing the firstResponder therefore it is not updated.
My suggestion is to use add target for this case. This is what you can do:
override func viewDidLoad() {
super.viewDidLoad()
// calling the function that initialize textFields
initializeTextFields()
accessCardTextField.addTarget(self, action: #selector(LoginViewController.accessCardTextFieldChanged(_:)), forControlEvents: .EditingChanged)
}
func accessCardTextFieldChanged(textField: UITextField) {
if textField.text?.characters.count == ACCESSCARD_MAXLENGTH {
modelTextField.becomeFirstResponder()
}
}
This way, it save you quite a few line of code. Most importantly, only accessCardTextField changed will be call. You could do another function to check your password textfield length separately. Also, i renamed from AccessCardTextField to accessCardTextField. It is recommended to have variable starting with lower case.
Not very sure if this works, but try edit your if statement contents with this.
if (AccessCardtextFieldLength == ACCESSCARD_MAXLENGTH){
if (textField == AccessCardTextField) {
textField.resignFirstResponder()
PasswordTextField.becomeFirstResponder()
}
}
In this condition
if (AccessCardtextFieldLength == ACCESSCARD_MAXLENGTH){
PasswordTextField.becomeFirstResponder()
**return true**
}
else{
return true
}
you returning flase that's why it doesn't show your last Character.