OTP Code View
Two years ago, I did a view for this purpose. Detail is on my github: https://github.com/nguyentruongky/ActiveCode.
It’s 100% code, without auto layout. Today, I make another view, with same purpose, but use auto layout, better UI and much easier to maintain.
UI will be like this.
You can try it yourself before follow this tutorial. Find your solution, that can be much better than this.
The solution idea:
- Add number of labels to display input letters.
- Add a textfield to show the keyboard, but set it be overlapped by other view (or out of frame)
- Capture every character input and update the labels.
Let's do it.
Initilize view
Init view with number of digits you need and an action to validate your OTP from your controller. The number of digits should be less than 6 for good UI.
private var digitCount = 0
private var validate: ((String) -> Void)?
convenience init(digitCount: Int, validate: @escaping ((String) -> Void)) {
self.init(frame: CGRect.zero)
self.digitCount = digitCount
self.validate = validate
setupView()
}
- Init the view with number of digits and a callback to validate the code input.
Setup UI
override func setupView() {
// (1)
guard digitCount > 0 else { return }
// (2)
var constraints = "H:|-8-"
for i in 0 ..< digitCount {
let label = makeLabel()
if i > 0 {
label.width(toView: labels[0])
}
constraints += "[v\(i)]-8-"
}
constraints += "|"
addConstraints(withFormat: constraints, arrayOf: labels)
height(60)
// (3)
setCode(at: 0, active: true)
hiddenTextField.becomeFirstResponder()
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(becomeFirstResponder)))
}
private func makeLabel() -> UILabel {
let label = knUIMaker.makeLabel(font: UIFont.systemFont(ofSize: 45),
color: color_69_125_245,
alignment: .center)
// (4)
label.createRoundCorner(5)
label.createBorder(0.5, color: color_102)
// (5)
addSubview(label)
label.vertical(toView: self)
labels.append(label)
return label
}
(1)
- Prevent code running while no digit. Only support init view by code with
init(digitCount:validate)
, so should prevent other init way to make wrong behaviour.
(2)
-
Set Auto layout code. Create a Visual Format Language string (like this:
"H:|-8-[v0]-[v1]-8-|"
). Detail is here. -
To make sure all digit indicators have same width, I set widthConstraint equal to the first indicator.
(3)
- Minor stuffs, make first indicator active, set focus to the hiddenTextField to show keyboard.
(4)
- This is how the indicator look. Want to change UI, just do it here.
(5)
- This is the label to display the indicator.
Set hidden UITextField
Add a UITextField, which is overlapped to use its keyboard and delegate. Every key input, I will update the digit indicator by catch the character in UITextFieldDelegate
lazy var hiddenTextField = addHiddenTextField()
private func addHiddenTextField() -> UITextField {
let tf = UITextField()
tf.translatesAutoresizingMaskIntoConstraints = false
tf.keyboardType = .numberPad
tf.isHidden = true
tf.delegate = self
addSubviews(views: tf)
tf.fill(toView: self)
return tf
}
override func becomeFirstResponder() -> Bool {
hiddenTextField.becomeFirstResponder()
return true
}
Conform UITextFieldDelegate
and update method textfield(shouldChangeCharactersIn:replacementString)
. This is the most important thing in this class.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var newText = string
// (1)
if isInvalid {
isInvalid = false
} else {
newText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
}
// (2)
let codeLength = newText.length
guard codeLength <= digitCount else { return false }
textField.text = newText
// (3)
func setTextToActiveBox() {
for i in 0 ..< codeLength {
let char = textField.text!.substring(from: i, to: i)
labels[i].text = char
setCode(at: i, active: true)
}
}
// (4)
func setTextToInactiveBox() {
for i in codeLength ..< digitCount {
labels[i].text = ""
setCode(at: i, active: false)
}
if codeLength <= digitCount - 1 {
setCode(at: codeLength, active: true)
}
}
setTextToActiveBox()
setTextToInactiveBox()
if codeLength == digitCount {
validateCode(code: textField.text!)
}
return false
}
(1)
- Reset all boxes when the code is invalid and type new character.
(2)(3)(4)
- Update every character from
hiddenTextField
to every indicator box. Rest of boxes will be set to empty.
Result
The main part is ready. Minors methods, properties are in the lib in github. Link is https://github.com/nguyentruongky/knOtpView.