I am building an iPhone app where users can enter a 6-digit verification code, each digit in a separate UITextField. The UI is created programmatically with UIStackView to arrange the text fields horizontally.
However, I am facing an issue with the backspace functionality. When the cursor is in the second placeholder and the backspace is pressed, the cursor should move to the first placeholder and delete its content. It unfortunately does not work. This works for other text fields but not for the first placeholder.
Code: import UIKit
class SignUpPhoneVerificationViewController: UIViewController, UITextFieldDelegate {
let codeTextFieldStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = 10
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.addSubview(codeTextFieldStackView)
for i in 0..<6 {
let textField = BackspaceTextField()
textField.borderStyle = .roundedRect
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 24)
textField.keyboardType = .numberPad
textField.delegate = self
textField.tag = i
textField.translatesAutoresizingMaskIntoConstraints = false
codeTextFieldStackView.addArrangedSubview(textField)
}
// Set up constraints for codeTextFieldStackView (not shown for brevity)
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Only allow numbers
guard CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string)) || string.isEmpty else {
return false
}
if string.isEmpty {
// Backspace was pressed
if textField.text?.isEmpty ?? true {
if let previousTextField = view.viewWithTag(textField.tag - 1) as? UITextField {
previousTextField.text = ""
previousTextField.becomeFirstResponder()
} else {
// Move cursor to the first text field if it's the second field
if textField.tag == 1, let firstTextField = view.viewWithTag(0) as? UITextField {
firstTextField.text = ""
firstTextField.becomeFirstResponder()
}
}
} else {
textField.text = ""
}
return false
}
if let text = textField.text, (text + string).count > 1 {
return false
}
textField.text = string
let nextTag = textField.tag + 1
if let nextResponder = view.viewWithTag(nextTag) {
nextResponder.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return false
}
}
class BackspaceTextField: UITextField {
override func deleteBackward() {
if text?.isEmpty ?? true {
if let previousTextField = superview?.viewWithTag(tag - 1) as? UITextField {
previousTextField.text = ""
previousTextField.becomeFirstResponder()
} else {
// Move cursor to the first text field if it's the second field
if tag == 1, let firstTextField = superview?.viewWithTag(0) as? UITextField {
firstTextField.text = ""
firstTextField.becomeFirstResponder()
} else {
super.deleteBackward()
}
}
} else {
text = ""
}
}
}
Expectation: When the cursor is in the second placeholder and the backspace key is pressed, the cursor should move to the first placeholder and clear its content.
Actual Action: The cursor does not move to the first placeholder and clear its content. The first placeholder retains its previous value.
Using the .tag
property is generally a bad idea... it can be useful, but it can also result in unexpected problems.
By default, UI elements inheriting from UIView
(which is pretty much every element, including UITextField
), have a default .tag
of 0
From Apple's docs:
Discussion
This method searches the current view and all of its subviews for the specified view.
So, superview?.viewWithTag(0)
returns the superview.
If you want to keep using .tag
to track your text fields, change this line:
textField.tag = i
to:
textField.tag = i + 1
Now your text field tags will be 1 to 6 instead of 0 to 5.
Another (perhaps better) approach would be to:
BackspaceTextField
from the superview's subviews.firstIndex(of: self)
index - 1
Something along these lines:
class BackspaceTextField: UITextField {
override func deleteBackward() {
// in case we moved the caret in front of the entered digit
if !(text ?? "").isEmpty {
text = ""
return
}
// make sure we have a superview
guard let sv = superview else {
text = ""
return
}
// get array of superview's subviews that are BackspaceTextField
let rtfArray = sv.subviews.compactMap{$0 as? BackspaceTextField}
guard !rtfArray.isEmpty else {
text = ""
return
}
// get index of self, and make sure it's not the first text field
guard let idx = rtfArray.firstIndex(of: self),
idx > 0
else {
text = ""
return
}
// clear the text of the previous field and set it as first responder
rtfArray[idx - 1].text = ""
rtfArray[idx - 1].becomeFirstResponder()
}
}