How to make digit passcode input in Swift

Issue #347

Add a hidden UITextField to view hierarchy, and add UITapGestureRecognizer to activate that textField.

Use padding string with limit to the number of labels, and prefix to get exactly n characters.

code

DigitView.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import UIKit

final class DigitView: UIView {
lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.distribution = .equalSpacing
return view
}()

private(set) var boxes: [UIView] = []
private(set) var labels: [UILabel] = []

lazy var hiddenTextField: UITextField = {
let textField = UITextField()
textField.alpha = 0
textField.keyboardType = .numbersAndPunctuation
return textField
}()

lazy var tapGR = UITapGestureRecognizer(target: self, action: #selector(handle(_:)))

override init(frame: CGRect) {
super.init(frame: frame)
setup()

addGestureRecognizer(tapGR)
}

required init?(coder aDecoder: NSCoder) {
fatalError()
}

override func layoutSubviews() {
super.layoutSubviews()

boxes.forEach {
$0.layer.borderWidth = 1
$0.layer.borderColor = R.color.primary.cgColor
$0.layoutIfNeeded()
$0.layer.cornerRadius = $0.bounds.height / 2
}
}

@objc private func handle(_ tapGR: UITapGestureRecognizer) {
hiddenTextField.becomeFirstResponder()
}

private func setup() {
addSubviews([hiddenTextField, stackView])
boxes = Array(0..<6).map { _ in
return UIView()
}

labels = boxes.map { box in
let label = UILabel()
label.font = R.customFont.semibold(16)
label.textAlignment = .center
label.textColor = R.color.primary
box.addSubview(label)

NSLayoutConstraint.on([
label.centerXAnchor.constraint(equalTo: box.centerXAnchor),
label.centerYAnchor.constraint(equalTo: box.centerYAnchor)
])

return label
}

boxes.forEach {
stackView.addArrangedSubview($0)

NSLayoutConstraint.on([
$0.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 0.9),
$0.widthAnchor.constraint(equalTo: $0.heightAnchor, multiplier: 1.0)
])
}

NSLayoutConstraint.on([
stackView.pinEdges(view: self, inset: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: -16))
])
}
}

DigitHandler.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
final class DigitHandler: NSObject {
let digitView: DigitView

init(digitView: DigitView) {
self.digitView = digitView
super.init()

digitView.hiddenTextField.delegate = self
digitView.hiddenTextField.addTarget(self, action: #selector(handle(_:)), for: .editingChanged)
}

@objc private func handle(_ textField: UITextField) {
guard let text = textField.text else {
return
}

let count = digitView.labels.count
let paddedText = String(text.padding(toLength: count, withPad: "-", startingAt: 0).prefix(count))
zip(digitView.labels, paddedText).forEach { tuple in
tuple.0.text = String(tuple.1)
}
}
}

extension DigitHandler: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let text = textField.text ?? ""
return text.count < digitView.labels.count
}
}

Comments