I am trying to have it so that when a user presses the "backspace/delete" button when they're inputting a PIN number, it goes to and deletes the "previous" textfield. I saw other solutions on SO and the function keyboardInputShouldDelete seemed promising but it didn't work. Any help is greatly appreciated. Below is my code:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if ((textField.text?.count)! < 1) && (string.count > 0) {
if textField == txtOTP1 {
txtOTP2.becomeFirstResponder()
}
if textField == txtOTP2 {
txtOTP3.becomeFirstResponder()
}
if textField == txtOTP3 {
txtOTP4.becomeFirstResponder()
}
if textField == txtOTP4 {
check()
}
textField.text = string
userPIN = "\(self.txtOTP1.text!)\(self.txtOTP2.text!)\(self.txtOTP3.text!)\(self.txtOTP4.text!)"
return false
} else if ((textField.text?.count)! >= 1) && (string.count == 0) {
if textField == txtOTP4 {
txtOTP3.becomeFirstResponder()
}
if textField == txtOTP3 {
txtOTP2.becomeFirstResponder()
}
if textField == txtOTP2 {
txtOTP1.becomeFirstResponder()
}
if textField == txtOTP1 {
}
textField.text = ""
return false
}
else if (textField.text?.count)! >= 1 {
textField.text = string
return false
}
return true
}
func keyboardInputShouldDelete(_ textField: UITextField) -> Bool {
textField.delegate = self
return true
}
Try do it like this:
//Get new length of the text after input
let newLength = (textField.text ?? "").count + string.count - range.length
if newLength == 0 {
//Go back prev textfield if new length is 0
}
To overcome the issue where the shouldChangeCharactersIn won't get called on empty field, simply carry the last character user input from last textfield to the next, so user will never have an empty field aside from the first textfield.
Another way is add a space character by default for each textfield and detect around it but it seems to be more annoying to dealt with.
Related
I know that if we want to auto fetch the OTP(if we use single textfield) we need to use
otpTextField.textContentType = .oneTimeCode
But, If we use multiple textfield(According to following image)
how should we achieve this ?
-> From iOS 12 Apple will allow the support to read One Time Code which you will get in the iPhone device. you can split text into four fields and autofilled and manually enter otp and remove one by one and move each textfield.
1) self.textone maxmimum length 4 and other textfield max length 1
2) Add UITextFieldDelegate
if #available(iOS 12.0, *) {
txtOne.textContentType = .oneTimeCode
}
self.txtOne.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
self.txtOne.becomeFirstResponder()
#objc func textFieldDidChange(_ textField: UITextField) {
if #available(iOS 12.0, *) {
if textField.textContentType == UITextContentType.oneTimeCode{
//here split the text to your four text fields
if let otpCode = textField.text, otpCode.count > 3{
txtOne.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 0)])
txtTwo.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 1)])
txtThree.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 2)])
txtFour.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 3)])
}
}
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if (string.count == 1){
if textField == txtOne {
txtTwo?.becomeFirstResponder()
}
if textField == txtTwo {
txtThree?.becomeFirstResponder()
}
if textField == txtThree {
txtFour?.becomeFirstResponder()
}
if textField == txtFour {
txtFour?.resignFirstResponder()
textField.text? = string
//APICall Verify OTP
//Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.VerifyOTPAPI), userInfo: nil, repeats: false)
}
textField.text? = string
return false
}else{
if textField == txtOne {
txtOne?.becomeFirstResponder()
}
if textField == txtTwo {
txtOne?.becomeFirstResponder()
}
if textField == txtThree {
txtTwo?.becomeFirstResponder()
}
if textField == txtFour {
txtThree?.becomeFirstResponder()
}
textField.text? = string
return false
}
}
I was stuck with Firebase OneTimeCode in 6 different UITextFields and manage to allow the OS to autofill it from Text Message, also to allow the user to copy and paste it and of course to allow the user to insert it one by one by implementing shouldChangeCharactersIn in a very manual but effective way:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//This lines allows the user to delete the number in the textfield.
if string.isEmpty{
return true
}
//----------------------------------------------------------------
//This lines prevents the users from entering any type of text.
if Int(string) == nil {
return false
}
//----------------------------------------------------------------
//This lines lets the user copy and paste the One Time Code.
//For this code to work you need to enable subscript in Strings https://gist.github.com/JCTec/6f6bafba57373f7385619380046822a0
if string.count == 6 {
first.text = "\(string[0])"
second.text = "\(string[1])"
third.text = "\(string[2])"
fourth.text = "\(string[3])"
fifth.text = "\(string[4])"
sixth.text = "\(string[5])"
DispatchQueue.main.async {
self.dismissKeyboard()
self.validCode()
}
}
//----------------------------------------------------------------
//This is where the magic happens. The OS will try to insert manually the code number by number, this lines will insert all the numbers one by one in each TextField as it goes In. (The first one will go in normally and the next to follow will be inserted manually)
if string.count == 1 {
if (textField.text?.count ?? 0) == 1 && textField.tag == 0{
if (second.text?.count ?? 0) == 1{
if (third.text?.count ?? 0) == 1{
if (fourth.text?.count ?? 0) == 1{
if (fifth.text?.count ?? 0) == 1{
sixth.text = string
DispatchQueue.main.async {
self.dismissKeyboard()
self.validCode()
}
return false
}else{
fifth.text = string
return false
}
}else{
fourth.text = string
return false
}
}else{
third.text = string
return false
}
}else{
second.text = string
return false
}
}
}
//----------------------------------------------------------------
//This lines of code will ensure you can only insert one number in each UITextField and change the user to next UITextField when function ends.
guard let textFieldText = textField.text,
let rangeOfTextToReplace = Range(range, in: textFieldText) else {
return false
}
let substringToReplace = textFieldText[rangeOfTextToReplace]
let count = textFieldText.count - substringToReplace.count + string.count
if count == 1{
if textField.tag == 0{
DispatchQueue.main.async {
self.second.becomeFirstResponder()
}
}else if textField.tag == 1{
DispatchQueue.main.async {
self.third.becomeFirstResponder()
}
}else if textField.tag == 2{
DispatchQueue.main.async {
self.fourth.becomeFirstResponder()
}
}else if textField.tag == 3{
DispatchQueue.main.async {
self.fifth.becomeFirstResponder()
}
}else if textField.tag == 4{
DispatchQueue.main.async {
self.sixth.becomeFirstResponder()
}
}else {
DispatchQueue.main.async {
self.dismissKeyboard()
self.validCode()
}
}
}
return count <= 1
//----------------------------------------------------------------
}
Note: I use a subscript string method in this code, you can get this extension here, String+Subscript.swift
And of course don't forget to assign the delegate and the .oneTimeCode to the TextField.
textField.delegate = self
textField.textContentType = .oneTimeCode
If you can get the auto OTP for single field, you can split that text into your four text fields. I believe.
You may have to use textField's change observer as like below,
textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
func textFieldDidChange(_ textField: UITextField) {
// here check you text field's input Type
if textField.textContentType == UITextContentType.oneTimeCode{
//here split the text to your four text fields
if let otpCode = textField.text, otpCode.count > 3{
textField.text = String(otpCode[otpCode.startIndex])
textField1.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 1)])
textField2.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 2)])
textField3.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 3)])
}
}
}
What I do is similar to #Natarajan's answer, but I use UITextFieldDelegate method. On viewDidAppear your first text field should become first responder and be of type oneTimeCode.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Fill your textfields here
return true
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.length > 1 {
textFieldDidChange(textField, otpCode: string)
return false
}
}
func textFieldDidChange(_ textField: UITextField, otpCode: String) {
if textField.textContentType == UITextContentType.oneTimeCode{
//here split the text to your four text fields
if otpCode.count == 4, Int(otpCode) != nil {
otp_field_1.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 0)])
otp_field_2.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 1)])
otp_field_3.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 2)])
otp_field_4.text = String(otpCode[otpCode.index(otpCode.startIndex, offsetBy: 3)])
let textFields = [otp_field_1, otp_field_2, otp_field_3, otp_field_4]
for i in 0..<textFields.count{
textFields[i].layer.borderColor = UIColor.GREEN_COLOR.cgColor
}
} else {
textField.text = ""
}
textField.resignFirstResponder()
}
}
I wrote this code for 4 textfields accepting each of the four characters for OTP. However I also want the user to avoid going to a random textfield themselves:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if(((textField.text?.count)! < 1) && (string.count > 0))
{
if textField == charForOTP1
{
charForOTP2.becomeFirstResponder()
}
else if textField == charForOTP2
{
charForOTP3.becomeFirstResponder()
}
else if textField == charForOTP3
{
charForOTP4.becomeFirstResponder()
}
else if textField == charForOTP4
{
charForOTP4.resignFirstResponder()
}
textField.text = string
return false
}
else if( (textField.text?.count)! >= 1) && (string.count == 0)
{
if textField == charForOTP2
{
charForOTP1.becomeFirstResponder()
}
else if textField == charForOTP3
{
charForOTP2.becomeFirstResponder()
}
else if textField == charForOTP4
{
charForOTP3.becomeFirstResponder()
}
else if textField == charForOTP1
{
charForOTP1.becomeFirstResponder()
}
textField.text = ""
return false
}
else if (textField.text?.count)! >= 1
{
textField.text = string
return false
}
return true;
}
I have tried implementing the same. However it resulted in some unexpected results.
What exactly should I add here for the same to work?
I hope your requirement is as:
First, you need only one textfield active, as soon as user inputted value next respective textfield will become active when reached to the last textfield you want to enable all textfields.
We need to add notification when textfield value changed in viewDidLoad method. (I have taken otp1, otp2, otp3, opt4 textfields also assign tags from 1 to 4 to all textfield).
otp1.addTarget(self, action: #selector(textFieldDidChange(textField:)), for: .editingChanged)
otp2.addTarget(self, action: #selector(textFieldDidChange(textField:)), for: .editingChanged)
otp3.addTarget(self, action: #selector(textFieldDidChange(textField:)), for: .editingChanged)
otp4.addTarget(self, action: #selector(textFieldDidChange(textField:)), for: .editingChanged)
I have written updateTextFields(textfield: UITextField) function which will updates whatever actions need to give to active/inactive textfield. To active first textfield we need to call self.updateTextFields(textfield: otp1) from viewDidLoad.
/// Updates actions for current textfield
///
/// - Parameter textfield: 'UITextField' object
func updateTextFields(textfield: UITextField) {
var enableArray = [UITextField]()
var disableArray = [UITextField]()
var sTextField: UITextField = UITextField()
switch textfield.tag {
case 1:
sTextField = otp1
enableArray.append(otp1)
disableArray.append(otp2)
disableArray.append(otp3)
disableArray.append(otp4)
break
case 2:
sTextField = otp2
enableArray.append(otp2)
disableArray.append(otp1)
disableArray.append(otp3)
disableArray.append(otp4)
break
case 3:
sTextField = otp3
enableArray.append(otp3)
disableArray.append(otp2)
disableArray.append(otp1)
disableArray.append(otp4)
break
case 4:
sTextField = otp4
enableArray.append(otp4)
enableArray.append(otp1)
enableArray.append(otp2)
enableArray.append(otp3)
break
}
for ltextfield in disableArray {
ltextfield.isEnabled = false
}
for ltextfield in enableArray {
ltextfield.isEnabled = true
}
sTextField.becomeFirstResponder()
}
Now the actual UITextFieldDelegate method to check the inputted character.
// MARK: UITextField Delegate Method
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string != "" {
var text = textField.text ?? ""
text.append(string)
if text.count > 1 {
return false
}
let allowedCharacters = CharacterSet(charactersIn:"0123456789 ")//Here change this characters based on your requirement
let characterSet = CharacterSet(charactersIn: string)
return allowedCharacters.isSuperset(of: characterSet)
}
return true
}
At last, our notification method which we have added in viewDidLoad method. Here we have checked textfield count and based on that we have move curser.
#objc private func textFieldDidChange(textField: UITextField) {
if textField.text?.count == 1, (textField.tag + 1) < 5 {
self.updateTextFields(textfield: self.getTextField(isForward: true, currentTextField: textField))
} else {
if textField.text?.count == 0, (textField.tag - 1) > 0 {
self.updateTextFields(textfield: self.getTextField(isForward: false, currentTextField: textField))
}
}
}
Hope this will helps you.
EDIT
Added Get Text field Method
/// Get TextField Object For Responder which is either active or resign
///
/// - Parameters:
/// - isForward: flag used to find whether we want next or previous text field
/// - currentTextField: TextField which is active
/// - Returns: TextField if it satisfied condition
func getTextField(isForward: Bool, currentTextField: UITextField) -> UITextField {
if isForward {
if (currentTextField.tag + 1) < 5 {
switch currentTextField.tag {
case 1:
return otp2
case 2:
return otp3
case 3:
return otp4
default:
break
}
}
} else {
if (currentTextField.tag - 1) > 0 {
switch currentTextField.tag {
case 4:
return otp3
case 2:
return otp1
case 3:
return otp2
default:
break
}
}
}
return UITextField()
}
i'm trying develop a textfield switching from 1 to another automatically, similarly i can switch the textfield backwards, but in null state i can't switch the cursor into the previous textfield , i tried with EditingChange Event but when the textfield in null state the Editng Change function is not fired.
The code is :
#IBAction func textFieldEditingChanged(_ sender: UITextField) {
if(sender.text?.characters.count == 1) {
switch sender {
case otpField1:
otpField2.becomeFirstResponder()
case otpField2:
otpField3.becomeFirstResponder()
case otpField3:
otpField4.becomeFirstResponder()
case otpField4:
otpField4.resignFirstResponder()
default:
break
}
}else {
}
if ((sender.text?.characters.count)! <= 0 || sender.text! == "" || sender.text?.isEmpty == true) {
switch sender {
case otpField4:
otpField3.becomeFirstResponder()
case otpField3:
otpField2.becomeFirstResponder()
case otpField2:
otpField1.becomeFirstResponder()
case otpField1:
otpField1.resignFirstResponder()
default:
break
}
}
}
In order find UITextField empty you can use isEmpty of String.
So here it will be:
sender.text.isEmpty
How I will use is
guard let email = emailTextField.text, !email.isEmpty else {
return
}
Hopes this solves your problem.
Rather implement the textfield delegate shouldChangeCharactersInRange and check in there, probably need to check for nil and some more stuff but this is the idea:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let newLength = (textField.text?.characters.count)! - range.length + string.characters.count
if (textField.text?.characters.count)! > 0 && newLength == 0 {
textField.text = ""
switch textField {
....
}
}
return true
}
You can find with condition like
if textField.isEmpty or textField.text == "" {
// Do your stuff here when textField is empty
}
else {
// Do your stuff when textField is not empty
}
I have a series of textfields in a single row. Each textfield is limited to two characters.
My problem is that I want to delete all the characters and go back through all the textfields until I get to the first textfield.
I have subclassed the textfield and overridden the deleteBackward method to determine if the text in the textfield is empty. If so then change the responder to the previous textfield.
This is all fine, but when there is one character in the textfield and backspace is pressed, the character is deleted and the responder is changed to the previous textfield.
This is incorrect behaviour as I want the focus to stay on that textfield that has had its character deleted and if the delete key is pressed again, then responder should change to previous textfield.
I corrected this my setting a flag in the shouldChangeCharacters delegate method of the textfield. The flag is set to true if ...
if (newLength == 0 && currentLength == 0) || currentLength == 0 {
textFieldEmpty = true
} else {
textFieldEmpty = false
}
This works fine, but when the textfield is empty, and backspace is pressed, this delegate method is not called?
Just wandering if anyone has solved a similar problem and what approach they may have taken?
Thanks
I have a similar scenario but with single characters in every text field. I have also used deleteBackward in a custom textField to achieve this.
Try this code:
import UIKit
protocol CustomTextFieldDelegate{
func textFieldDidDelete()
}
class CustomTextField: UITextField {
var myCustomTextFieldDelegate : CustomTextFieldDelegate!
override func deleteBackward() {
super.deleteBackward()
myCustomTextFieldDelegate.textFieldDidDelete()
}
}
And this is how you should implement the deleteBackward method:
func textFieldDidDelete() {
print("Entered Delete");
print("Tag!!\(currentActiveTextField.tag)")
var previousTag = 0
if currentActiveTextField.text == ""{
previousTag = currentActiveTextField.tag - 1
}else{
previousTag = currentActiveTextField.tag
}
let previousResponder = currentActiveTextField.superview?.viewWithTag(previousTag)
if(previousTag > 0){
let previousTextField = previousResponder as! CustomTextField
previousTextField.text = ""
currentActiveTextField = previousTextField
previousResponder?.becomeFirstResponder()
}
}
Also in shouldChangeCharacters in range method, dont forget to set your currentlyActiveTextField as the text field you wish to edit. Check the following code, might help. My text fields have tags 1,2,3,4.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
currentActiveTextField = textField as! CustomTextField
let nextTag = textField.tag + 1;
// get next responder
var nextResponder = textField.superview?.viewWithTag(nextTag);
// On inputing value to textfield
if (textField.text?.characters.count < 1 && string.characters.count > 0){
if (nextResponder == nil){
nextResponder = textField.superview?.viewWithTag(1);
}
textField.text = string;
if nextTag == 5 {
self.digitFourTextField.resignFirstResponder()
return true;
}
nextResponder?.becomeFirstResponder();
return false;
}
if (textField.text?.characters.count >= 1 && string.characters.count == 0){
// on deleteing value from Textfield
let presentTag = textField.tag;
// get next responder
var presentResponder = textField.superview?.viewWithTag(presentTag);
if (presentResponder == nil){
presentResponder = textField.superview?.viewWithTag(1);
}
textField.text = "";
presentResponder?.becomeFirstResponder();
return false;
}
let currentCharacterCount = textField.text?.characters.count ?? 0
let newLength = currentCharacterCount + string.characters.count - range.length
if newLength > 1 {
textField.text = string
if (nextResponder != nil){
nextResponder?.becomeFirstResponder();
}
}
let returnVal = newLength <= 1
return returnVal;
}
Call the delegate method and then call super.deleteBackwards() after.
This way, at the time you call the delegate method the text.count == 1. If you flip the order and call super.deleteBackwards() first the text.count == 0 and you cannot accurately check for an empty string.
I have 4 UItextfield for enter the top number. Like 1 2 3 4. When I enter the top number in my all text field, and if I am in last urtext field - and if I press backward button or x key in my key board - My number are in each text field are not getting deleted. I was not able to delete.
Here is my code :
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let value = textField.text!
let length = value.characters.count
if (length == 1) {
return false
}
if (textField == Field1) {
self.performSelector(Selector("setNextResponder:"), withObject: Field2, afterDelay: 0.2)
textField1 = string
} else if (textField == Field2) {
self.performSelector(Selector("setNextResponder:"), withObject: Field3, afterDelay: 0.2)
textField2 = string
} else if (textField == Field3) {
self.performSelector(Selector("setNextResponder:"), withObject: Field4, afterDelay: 0.2)
textField3 = string
} else if (textField == Field4) {
textField.resignFirstResponder()
textField4 = string
nextButton.enabled = true
}
else if (textField == Field4)
{
if string == "" && textField.text?.characters.count == 1 {
Field1.text = ""
Field2.text = ""
Field3.text = ""
Field4.text = ""
Field1.becomeFirstResponder()
}
}
return true
}
What i am missing?
You can make it like this:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if textField.text?.characters.count == 1 && string == "" {
if textField == txt2 {
txt2.text = ""
txt1.becomeFirstResponder()
} else if textField == txt3 {
txt3.text = ""
txt2.becomeFirstResponder()
} else if textField == txt4 {
txt4.text = ""
txt3.becomeFirstResponder()
}
}
return true
}
Still if possible you should create a separate button with clearText action to clear all textfield instead of clearing the textfield from the clear button. It will look as better UI/UX as per the default iOS clear button behaviour
Try :
func textFieldShouldClear(textField: UITextField) -> Bool {
textField.text = ""
textField.resignFirstResponder()
return false
}
Also choose an appropriate option from attribute inspector.