-
Notifications
You must be signed in to change notification settings - Fork 100
Expand file tree
/
Copy pathPinCodeTextField.swift
More file actions
341 lines (292 loc) · 10.8 KB
/
PinCodeTextField.swift
File metadata and controls
341 lines (292 loc) · 10.8 KB
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
//
// PinCodeTextField.swift
// PinCodeTextField
//
// Created by Alexander Tkachenko on 3/15/17.
// Copyright © 2017 organization. All rights reserved.
//
import Foundation
import UIKit
@IBDesignable public class PinCodeTextField: UIView {
public weak var delegate: PinCodeTextFieldDelegate?
//MARK: Customizable from Interface Builder
@IBInspectable public var underlineWidth: CGFloat = 40
@IBInspectable public var underlineHSpacing: CGFloat = 10
@IBInspectable public var underlineVMargin: CGFloat = 0
@IBInspectable public var characterLimit: Int = 5
@IBInspectable public var underlineHeight: CGFloat = 3
@IBInspectable public var placeholderText: String?
@IBInspectable public var text: String? {
didSet {
updateView()
}
}
@IBInspectable public var fontSize: CGFloat = 14 {
didSet {
font = font.withSize(fontSize)
}
}
@IBInspectable public var textColor: UIColor = UIColor.clear
@IBInspectable public var placeholderColor: UIColor = UIColor.lightGray
@IBInspectable public var underlineColor: UIColor = UIColor.darkGray
@IBInspectable public var updatedUnderlineColor: UIColor = UIColor.clear
@IBInspectable public var secureText: Bool = false
@IBInspectable public var needToUpdateUnderlines: Bool = true
@IBInspectable public var characterBackgroundColor: UIColor = UIColor.clear
@IBInspectable public var characterBackgroundCornerRadius: CGFloat = 0
//MARK: Customizable from code
public var keyboardType: UIKeyboardType = UIKeyboardType.alphabet
public var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default
public var autocorrectionType: UITextAutocorrectionType = UITextAutocorrectionType.no
public var font: UIFont = UIFont.systemFont(ofSize: 14)
public var allowedCharacterSet: CharacterSet = CharacterSet.alphanumerics
private var _inputView: UIView?
public override var inputView: UIView? {
get {
return _inputView
}
set {
_inputView = newValue
}
}
// UIResponder
private var _inputAccessoryView: UIView?
@IBOutlet public override var inputAccessoryView: UIView? {
get {
return _inputAccessoryView
}
set {
_inputAccessoryView = newValue
}
}
public var isSecureTextEntry: Bool {
get {
return secureText
}
@objc(setSecureTextEntry:) set {
secureText = newValue
}
}
//MARK: Private
private var labels: [UILabel] = []
private var underlines: [UIView] = []
private var backgrounds: [UIView] = []
//MARK: Init and awake
override init(frame: CGRect) {
super.init(frame: frame)
postInitialize()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override public func awakeFromNib() {
super.awakeFromNib()
postInitialize()
}
override public func prepareForInterfaceBuilder() {
postInitialize()
}
private func postInitialize() {
updateView()
}
//MARK: Overrides
override public func layoutSubviews() {
layoutCharactersAndPlaceholders()
super.layoutSubviews()
}
override public var canBecomeFirstResponder: Bool {
return true
}
@discardableResult override public func becomeFirstResponder() -> Bool {
delegate?.textFieldDidBeginEditing(self)
return super.becomeFirstResponder()
}
@discardableResult override public func resignFirstResponder() -> Bool {
delegate?.textFieldDidEndEditing(self)
return super.resignFirstResponder()
}
//MARK: Private
private func updateView() {
if needToRecreateBackgrounds() {
recreateBackgrounds()
}
if needToRecreateUnderlines() {
recreateUnderlines()
}
if needToRecreateLabels() {
recreateLabels()
}
updateLabels()
if needToUpdateUnderlines {
updateUnderlines()
}
updateBackgrounds()
setNeedsLayout()
}
private func needToRecreateUnderlines() -> Bool {
return characterLimit != underlines.count
}
private func needToRecreateLabels() -> Bool {
return characterLimit != labels.count
}
private func needToRecreateBackgrounds() -> Bool {
return characterLimit != backgrounds.count
}
private func recreateUnderlines() {
underlines.forEach{ $0.removeFromSuperview() }
underlines.removeAll()
characterLimit.times {
let underline = createUnderline()
underlines.append(underline)
addSubview(underline)
}
}
private func recreateLabels() {
labels.forEach{ $0.removeFromSuperview() }
labels.removeAll()
characterLimit.times {
let label = createLabel()
labels.append(label)
addSubview(label)
}
}
private func recreateBackgrounds() {
backgrounds.forEach{ $0.removeFromSuperview() }
backgrounds.removeAll()
characterLimit.times {
let background = createBackground()
backgrounds.append(background)
addSubview(background)
}
}
private func updateLabels() {
let textHelper = TextHelper(text: text, placeholder: placeholderText, isSecure: isSecureTextEntry)
for label in labels {
let index = labels.index(of: label) ?? 0
let currentCharacter = textHelper.character(atIndex: index)
label.text = currentCharacter.map { String($0) }
label.font = font
let isplaceholder = isPlaceholder(index)
label.textColor = labelColor(isPlaceholder: isplaceholder)
}
}
private func updateUnderlines() {
for label in labels {
let index = labels.index(of: label) ?? 0
if isPlaceholder(index) {
underlines[index].backgroundColor = underlineColor
}
else{
underlines[index].backgroundColor = updatedUnderlineColor
}
}
}
private func updateBackgrounds() {
for background in backgrounds {
background.backgroundColor = characterBackgroundColor
background.layer.cornerRadius = characterBackgroundCornerRadius
}
}
private func labelColor(isPlaceholder placeholder: Bool) -> UIColor {
return placeholder ? placeholderColor : textColor
}
private func isPlaceholder(_ i: Int) -> Bool {
let inputTextCount = text?.length ?? 0
return i >= inputTextCount
}
private func createLabel() -> UILabel {
let label = UILabel(frame: CGRect())
label.font = font
label.backgroundColor = UIColor.clear
label.textAlignment = .center
return label
}
private func createUnderline() -> UIView {
let underline = UIView()
underline.backgroundColor = underlineColor
return underline
}
private func createBackground() -> UIView {
let background = UIView()
background.backgroundColor = characterBackgroundColor
background.layer.cornerRadius = characterBackgroundCornerRadius
background.clipsToBounds = true
return background
}
private func layoutCharactersAndPlaceholders() {
let marginsCount = characterLimit - 1
let totalMarginsWidth = underlineHSpacing * CGFloat(marginsCount)
let totalUnderlinesWidth = underlineWidth * CGFloat(characterLimit)
var currentUnderlineX: CGFloat = bounds.width / 2 - (totalUnderlinesWidth + totalMarginsWidth) / 2
var currentLabelCenterX = currentUnderlineX + underlineWidth / 2
let totalLabelHeight = font.ascender + font.descender
let underlineY = bounds.height / 2 + totalLabelHeight / 2 + underlineVMargin
for i in 0..<underlines.count {
let underline = underlines[i]
let background = backgrounds[i]
underline.frame = CGRect(x: currentUnderlineX, y: underlineY, width: underlineWidth, height: underlineHeight)
background.frame = CGRect(x: currentUnderlineX, y: 0, width: underlineWidth, height: bounds.height)
currentUnderlineX += underlineWidth + underlineHSpacing
}
labels.forEach {
$0.sizeToFit()
let labelWidth = $0.bounds.width
let labelX = (currentLabelCenterX - labelWidth / 2).rounded(.down)
$0.frame = CGRect(x: labelX, y: 0, width: labelWidth, height: bounds.height)
currentLabelCenterX += underlineWidth + underlineHSpacing
}
}
//MARK: Touches
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let location = touch.location(in: self)
if (bounds.contains(location)) {
if (delegate?.textFieldShouldBeginEditing(self) ?? true) {
let _ = becomeFirstResponder()
}
}
}
//MARK: Text processing
func canInsertCharacter(_ character: String) -> Bool {
let newText = text.map { $0 + character } ?? character
let isNewline = character.hasOnlyNewlineSymbols
let isCharacterMatchingCharacterSet = character.trimmingCharacters(in: allowedCharacterSet).isEmpty
let isLengthWithinLimit = newText.length <= characterLimit
return !isNewline && isCharacterMatchingCharacterSet && isLengthWithinLimit
}
}
//MARK: UIKeyInput
extension PinCodeTextField: UIKeyInput {
public var hasText: Bool {
if let text = text {
return !text.isEmpty
}
else {
return false
}
}
public func insertText(_ charToInsert: String) {
if charToInsert.hasOnlyNewlineSymbols {
if (delegate?.textFieldShouldReturn(self) ?? true) {
let _ = resignFirstResponder()
}
}
else if canInsertCharacter(charToInsert) {
let newText = text.map { $0 + charToInsert } ?? charToInsert
text = newText
delegate?.textFieldValueChanged(self)
if (newText.length == characterLimit) {
if (delegate?.textFieldShouldEndEditing(self) ?? true) {
let _ = resignFirstResponder()
}
}
}
}
public func deleteBackward() {
guard hasText else { return }
text?.removeLastCharacter()
delegate?.textFieldValueChanged(self)
}
}