├── .gitignore
├── Package.swift
├── README.md
└── Sources
└── OneTimeCodeTextField
├── OneTimeCodeTextField.swift
├── OneTimeCodeTextFieldDelegate.swift
└── OneTimeCodeTextFieldExtension.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "OneTimeCodeTextField",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "OneTimeCodeTextField",
15 | targets: ["OneTimeCodeTextField"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "OneTimeCodeTextField",
26 | dependencies: [])
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OneTimeCodeTextField
2 |
3 |
4 |
5 |
6 | ### Example configuration
7 |
8 | ```
9 | class ViewController: UIViewController {
10 | @IBOutlet weak var codeTxt: OneTimeCodeTextField!
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 |
15 | // Configure has to be called first
16 | codeTxt.configure(withSlotCount: 6, andSpacing: 8) // Default: 6 slots, 8 spacing
17 |
18 |
19 | // Customisation(Optional)
20 | codeTxt.codeBackgroundColor = .secondarySystemBackground // Default: .secondarySystemBackground
21 | codeTxt.codeTextColor = .label // Default: .label
22 | codeTxt.codeFont = .systemFont(ofSize: 30, weight: .black) // Default: .system(ofSize: 24)
23 | codeTxt.codeMinimumScaleFactor = 0.2 // Default: 0.8
24 |
25 | codeTxt.codeCornerRadius = 12 // Default: 8
26 | codeTxt.codeCornerCurve = .continuous // Default: .continuous
27 |
28 | codeTxt.codeBorderWidth = 1 // Default: 0
29 | codeTxt.codeBorderColor = .label // Default: .none
30 |
31 | // Allow none-numeric code
32 | codeTxt.oneTimeCodeDelegate.allowedCharacters = .alphanumerics // Default: .decimalDigits
33 |
34 | //You should also specify which corresponding keyboard should be shown
35 | codeTxt.keyboardType = .asciiCapable // Default: .numberPad
36 |
37 | // Get entered Passcode
38 | codeTxt.didReceiveCode = { code in
39 | print(code)
40 | }
41 |
42 | // Clear textfield
43 | codeTxt.clear()
44 | }
45 | }
46 | ```
47 |
--------------------------------------------------------------------------------
/Sources/OneTimeCodeTextField/OneTimeCodeTextField.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public class OneTimeCodeTextField: UITextField {
4 | // MARK: UI Components
5 | public var digitLabels = [UILabel]()
6 |
7 | // MARK: Delegates
8 | public lazy var oneTimeCodeDelegate = OneTimeCodeTextFieldDelegate(oneTimeCodeTextField: self)
9 |
10 | // MARK: Properties
11 | private var isConfigured = false
12 | private lazy var tapGestureRecognizer: UITapGestureRecognizer = {
13 | let recognizer = UITapGestureRecognizer()
14 | recognizer.addTarget(self, action: #selector(becomeFirstResponder))
15 | return recognizer
16 | }()
17 |
18 | // MARK: Completions
19 | public var didReceiveCode: ((String) -> Void)?
20 |
21 | // MARK: Customisations
22 | /// Needs to be called after `configure()`.
23 | /// Default value: `.secondarySystemBackground`
24 | public var codeBackgroundColor: UIColor = .secondarySystemBackground {
25 | didSet {
26 | digitLabels.forEach({ $0.backgroundColor = codeBackgroundColor })
27 | }
28 | }
29 |
30 | /// Needs to be called after `configure()`.
31 | /// Default value: `.label`
32 | public var codeTextColor: UIColor = .label {
33 | didSet {
34 | digitLabels.forEach({ $0.textColor = codeTextColor })
35 | }
36 | }
37 |
38 | /// Needs to be called after `configure()`.
39 | /// Default value: `.systemFont(ofSize: 24)`
40 | public var codeFont: UIFont = .systemFont(ofSize: 24) {
41 | didSet {
42 | digitLabels.forEach({ $0.font = codeFont })
43 | }
44 | }
45 |
46 | /// Needs to be called after `configure()`.
47 | /// Default value: 0.8
48 | public var codeMinimumScaleFactor: CGFloat = 0.8 {
49 | didSet {
50 | digitLabels.forEach({ $0.minimumScaleFactor = codeMinimumScaleFactor })
51 | }
52 | }
53 |
54 | /// Needs to be called after `configure()`.
55 | /// Default value: 8
56 | public var codeCornerRadius: CGFloat = 8 {
57 | didSet {
58 | digitLabels.forEach({ $0.layer.cornerRadius = codeCornerRadius })
59 | }
60 | }
61 |
62 | /// Needs to be called after `configure()`.
63 | /// Default value: `.continuous`
64 | public var codeCornerCurve: CALayerCornerCurve = .continuous {
65 | didSet {
66 | digitLabels.forEach({ $0.layer.cornerCurve = codeCornerCurve })
67 | }
68 | }
69 |
70 | /// Needs to be called after `configure()`.
71 | /// Default value: 0
72 | public var codeBorderWidth: CGFloat = 0 {
73 | didSet {
74 | digitLabels.forEach({ $0.layer.borderWidth = codeBorderWidth })
75 | }
76 | }
77 |
78 | /// Needs to be called after `configure()`.
79 | /// Default value: `.none`
80 | public var codeBorderColor: UIColor? = .none {
81 | didSet {
82 | digitLabels.forEach({ $0.layer.borderColor = codeBorderColor?.cgColor })
83 | }
84 | }
85 |
86 | // MARK: Configuration
87 | public func configure(withSlotCount slotCount: Int = 6, andSpacing spacing: CGFloat = 8) {
88 | guard isConfigured == false else { return }
89 | isConfigured = true
90 | configureTextField()
91 |
92 | let slotsStackView = generateSlotsStackView(with: slotCount, spacing: spacing)
93 | addSubview(slotsStackView)
94 | addGestureRecognizer(tapGestureRecognizer)
95 |
96 | NSLayoutConstraint.activate([
97 | slotsStackView.topAnchor.constraint(equalTo: topAnchor),
98 | slotsStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
99 | slotsStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
100 | slotsStackView.bottomAnchor.constraint(equalTo: bottomAnchor)
101 | ])
102 | }
103 |
104 | private func configureTextField() {
105 | tintColor = .clear
106 | textColor = .clear
107 | layer.borderWidth = 0
108 | borderStyle = .none
109 | keyboardType = .numberPad
110 | textContentType = .oneTimeCode
111 |
112 | addTarget(self, action: #selector(textDidChange), for: .editingChanged)
113 | delegate = oneTimeCodeDelegate
114 |
115 | becomeFirstResponder()
116 | }
117 |
118 | private func generateSlotsStackView(with count: Int, spacing: CGFloat) -> UIStackView {
119 | let stackView = UIStackView()
120 | stackView.translatesAutoresizingMaskIntoConstraints = false
121 | stackView.axis = .horizontal
122 | stackView.alignment = .fill
123 | stackView.distribution = .fillEqually
124 | stackView.spacing = spacing
125 |
126 | for _ in 0.. UILabel {
136 | let label = UILabel()
137 | label.translatesAutoresizingMaskIntoConstraints = false
138 | label.isUserInteractionEnabled = true
139 | label.textAlignment = .center
140 | label.font = codeFont
141 | label.numberOfLines = 0
142 | label.adjustsFontSizeToFitWidth = true
143 | label.minimumScaleFactor = 0.8
144 | label.textColor = codeTextColor
145 | label.backgroundColor = codeBackgroundColor
146 |
147 | label.layer.masksToBounds = true
148 | label.layer.cornerRadius = codeCornerRadius
149 | label.layer.cornerCurve = codeCornerCurve
150 |
151 | label.layer.borderWidth = codeBorderWidth
152 | label.layer.borderColor = codeBorderColor?.cgColor
153 |
154 | return label
155 | }
156 |
157 | @objc
158 | private func textDidChange() {
159 | guard let code = text, code.count <= digitLabels.count else { return }
160 |
161 | for i in 0 ..< digitLabels.count {
162 | let currentLabel = digitLabels[i]
163 |
164 | if i < code.count {
165 | let index = code.index(code.startIndex, offsetBy: i)
166 | currentLabel.text = String(code[index]).uppercased()
167 | } else {
168 | currentLabel.text?.removeAll()
169 | }
170 | }
171 |
172 | if code.count == digitLabels.count { didReceiveCode?(code) }
173 | }
174 |
175 | public func clear() {
176 | guard isConfigured == true else { return }
177 | digitLabels.forEach({ $0.text = "" })
178 | text = ""
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Sources/OneTimeCodeTextField/OneTimeCodeTextFieldDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public class OneTimeCodeTextFieldDelegate: NSObject, UITextFieldDelegate {
4 |
5 | public var allowedCharacters: CharacterSet = .decimalDigits
6 |
7 | let oneTimeCodeTextField: OneTimeCodeTextField
8 |
9 | init(oneTimeCodeTextField: OneTimeCodeTextField) {
10 | self.oneTimeCodeTextField = oneTimeCodeTextField
11 | }
12 |
13 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
14 | guard allowedCharacters.isSuperset(of: CharacterSet(charactersIn: string)),
15 | let characterCount = textField.text?.count else { return false }
16 | return characterCount < oneTimeCodeTextField.digitLabels.count || string == ""
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/OneTimeCodeTextField/OneTimeCodeTextFieldExtension.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension OneTimeCodeTextField {
4 | public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
5 | false
6 | }
7 |
8 | public override func caretRect(for position: UITextPosition) -> CGRect {
9 | .zero
10 | }
11 |
12 | public override func selectionRects(for range: UITextRange) -> [UITextSelectionRect] {
13 | []
14 | }
15 | }
16 |
--------------------------------------------------------------------------------