Skip to content

Commit 6cb5b19

Browse files
committed
allow pasting code in field
1 parent dda8229 commit 6cb5b19

File tree

1 file changed

+113
-49
lines changed

1 file changed

+113
-49
lines changed

samples/auth-swiftui/AuthSwiftUIExample/Views/Phone/VerificationCodeInputField.swift

Lines changed: 113 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ struct VerificationCodeInputField: View {
4444
digit: $digitFields[index],
4545
isError: isError,
4646
isFocused: focusedIndex == index,
47+
maxDigits: codeLength - index,
4748
onDigitChanged: { newDigit in
4849
handleDigitChanged(at: index, newDigit: newDigit)
4950
},
@@ -53,7 +54,9 @@ struct VerificationCodeInputField: View {
5354
onFocusChanged: { isFocused in
5455
DispatchQueue.main.async {
5556
if isFocused {
56-
focusedIndex = index
57+
if focusedIndex != index {
58+
focusedIndex = index
59+
}
5760
} else if focusedIndex == index {
5861
focusedIndex = nil
5962
}
@@ -88,39 +91,61 @@ struct VerificationCodeInputField: View {
8891
}
8992

9093
private func handleDigitChanged(at index: Int, newDigit: String) {
91-
// Update the digit field only if it actually changed to avoid redundant state writes
92-
if digitFields[index] != newDigit {
93-
digitFields[index] = newDigit
94+
let sanitized = newDigit.filter { $0.isNumber }
95+
96+
guard !sanitized.isEmpty else {
97+
processSingleDigitInput(at: index, digit: "")
98+
return
99+
}
100+
101+
let firstDigit = String(sanitized.prefix(1))
102+
processSingleDigitInput(at: index, digit: firstDigit)
103+
104+
let remainder = String(sanitized.dropFirst())
105+
let availableSlots = max(codeLength - (index + 1), 0)
106+
if availableSlots > 0 {
107+
let trimmedRemainder = String(remainder.prefix(availableSlots))
108+
if !trimmedRemainder.isEmpty {
109+
applyBulkInput(startingAt: index + 1, digits: trimmedRemainder)
110+
}
111+
}
112+
}
113+
114+
private func processSingleDigitInput(at index: Int, digit: String) {
115+
if digitFields[index] != digit {
116+
digitFields[index] = digit
94117
}
95118

96-
// Update the main code string
97119
let newCode = digitFields.joined()
98120
code = newCode
99121
onCodeChange(newCode)
100-
101-
// Move to next empty field if digit was entered (only when adding, not removing)
102-
if !newDigit.isEmpty {
103-
if let nextIndex = findNextEmptyField(startingFrom: index) {
104-
DispatchQueue.main.async {
122+
123+
if !digit.isEmpty,
124+
let nextIndex = findNextEmptyField(startingFrom: index) {
125+
DispatchQueue.main.async {
126+
if focusedIndex != nextIndex {
105127
focusedIndex = nextIndex
106128
}
107129
}
108130
}
109131

110-
// Check if code is complete
111132
if newCode.count == codeLength {
112133
DispatchQueue.main.async {
113134
onCodeComplete(newCode)
114135
}
115136
}
116137
}
138+
117139

118140
private func handleBackspace(at index: Int) {
119141
// If current field is empty, move to previous field and clear it
120142
if digitFields[index].isEmpty && index > 0 {
121143
digitFields[index - 1] = ""
122144
DispatchQueue.main.async {
123-
focusedIndex = index - 1
145+
let previousIndex = index - 1
146+
if focusedIndex != previousIndex {
147+
focusedIndex = previousIndex
148+
}
124149
}
125150
} else {
126151
// Clear current field
@@ -133,6 +158,41 @@ struct VerificationCodeInputField: View {
133158
onCodeChange(newCode)
134159
}
135160

161+
private func applyBulkInput(startingAt index: Int, digits: String) {
162+
guard !digits.isEmpty, index < codeLength else { return }
163+
164+
var updatedFields = digitFields
165+
var currentIndex = index
166+
167+
for digit in digits where currentIndex < codeLength {
168+
updatedFields[currentIndex] = String(digit)
169+
currentIndex += 1
170+
}
171+
172+
if digitFields != updatedFields {
173+
digitFields = updatedFields
174+
}
175+
176+
let newCode = updatedFields.joined()
177+
code = newCode
178+
onCodeChange(newCode)
179+
180+
if newCode.count == codeLength {
181+
DispatchQueue.main.async {
182+
onCodeComplete(newCode)
183+
}
184+
} else {
185+
let clampedIndex = max(min(currentIndex - 1, codeLength - 1), 0)
186+
if let nextIndex = findNextEmptyField(startingFrom: clampedIndex) {
187+
DispatchQueue.main.async {
188+
if focusedIndex != nextIndex {
189+
focusedIndex = nextIndex
190+
}
191+
}
192+
}
193+
}
194+
}
195+
136196
private func findNextEmptyField(startingFrom index: Int) -> Int? {
137197
// Look for the next empty field after the current index
138198
for i in (index + 1)..<codeLength {
@@ -154,13 +214,23 @@ private struct SingleDigitField: View {
154214
@Binding var digit: String
155215
let isError: Bool
156216
let isFocused: Bool
217+
let maxDigits: Int
157218
let onDigitChanged: (String) -> Void
158219
let onBackspace: () -> Void
159220
let onFocusChanged: (Bool) -> Void
160-
161-
@State private var borderWidth: CGFloat = 1
162-
@State private var borderColor: Color = Color(.systemFill)
163-
221+
222+
private var borderWidth: CGFloat {
223+
if isError { return 2 }
224+
if isFocused || !digit.isEmpty { return 3 }
225+
return 1
226+
}
227+
228+
private var borderColor: Color {
229+
if isError { return .red }
230+
if isFocused || !digit.isEmpty { return .accentColor }
231+
return Color(.systemFill)
232+
}
233+
164234
var body: some View {
165235
BackspaceAwareTextField(
166236
text: $digit,
@@ -175,6 +245,7 @@ private struct SingleDigitField: View {
175245
onFocusChanged: { isFocused in
176246
onFocusChanged(isFocused)
177247
},
248+
maxCharacters: maxDigits,
178249
configuration: { textField in
179250
textField.font = .systemFont(ofSize: 24, weight: .medium)
180251
textField.textAlignment = .center
@@ -197,33 +268,6 @@ private struct SingleDigitField: View {
197268
)
198269
)
199270
.frame(maxWidth: .infinity)
200-
.onChange(of: digit) { _, _ in
201-
updateBorderAppearance()
202-
}
203-
.onChange(of: isFocused) { oldValue, newValue in
204-
updateBorderAppearance()
205-
}
206-
.onChange(of: isError) { oldValue, newValue in
207-
updateBorderAppearance()
208-
}
209-
.onAppear {
210-
updateBorderAppearance()
211-
}
212-
}
213-
214-
private func updateBorderAppearance() {
215-
withAnimation(.easeInOut(duration: 0.15)) {
216-
if isError {
217-
borderWidth = 2
218-
borderColor = .red
219-
} else if isFocused || !digit.isEmpty {
220-
borderWidth = 3
221-
borderColor = .accentColor
222-
} else {
223-
borderWidth = 1
224-
borderColor = Color(.systemFill)
225-
}
226-
}
227271
}
228272
}
229273

@@ -232,6 +276,7 @@ private struct BackspaceAwareTextField: UIViewRepresentable {
232276
var isFirstResponder: Bool
233277
let onDeleteBackwardWhenEmpty: () -> Void
234278
let onFocusChanged: (Bool) -> Void
279+
let maxCharacters: Int
235280
let configuration: (UITextField) -> Void
236281
let onTextChange: (String) -> Void
237282

@@ -267,10 +312,16 @@ private struct BackspaceAwareTextField: UIViewRepresentable {
267312
}
268313
}
269314

270-
if isFirstResponder && !uiView.isFirstResponder {
271-
uiView.becomeFirstResponder()
272-
} else if !isFirstResponder && uiView.isFirstResponder {
273-
uiView.resignFirstResponder()
315+
if isFirstResponder {
316+
if !context.coordinator.isFirstResponder {
317+
context.coordinator.isFirstResponder = true
318+
DispatchQueue.main.async { [weak uiView] in
319+
guard let uiView, !uiView.isFirstResponder else { return }
320+
uiView.becomeFirstResponder()
321+
}
322+
}
323+
} else if context.coordinator.isFirstResponder {
324+
context.coordinator.isFirstResponder = false
274325
}
275326
}
276327

@@ -280,6 +331,7 @@ private struct BackspaceAwareTextField: UIViewRepresentable {
280331

281332
final class Coordinator: NSObject, UITextFieldDelegate {
282333
var parent: BackspaceAwareTextField
334+
var isFirstResponder = false
283335

284336
init(parent: BackspaceAwareTextField) {
285337
self.parent = parent
@@ -292,10 +344,12 @@ private struct BackspaceAwareTextField: UIViewRepresentable {
292344
}
293345

294346
func textFieldDidBeginEditing(_ textField: UITextField) {
347+
isFirstResponder = true
295348
parent.onFocusChanged(true)
296349
}
297350

298351
func textFieldDidEndEditing(_ textField: UITextField) {
352+
isFirstResponder = false
299353
parent.onFocusChanged(false)
300354
}
301355

@@ -308,13 +362,23 @@ private struct BackspaceAwareTextField: UIViewRepresentable {
308362
return true
309363
}
310364

311-
guard string.allSatisfy({ $0.isNumber }) else {
365+
let digitsOnly = string.filter { $0.isNumber }
366+
guard !digitsOnly.isEmpty else {
312367
return false
313368
}
314369

315370
let currentText = textField.text ?? ""
316371
let nsCurrent = currentText as NSString
317-
let updated = nsCurrent.replacingCharacters(in: range, with: string)
372+
373+
if digitsOnly.count > 1 || string.count > 1 {
374+
let limit = max(parent.maxCharacters, 1)
375+
let truncated = String(digitsOnly.prefix(limit))
376+
let proposed = nsCurrent.replacingCharacters(in: range, with: truncated)
377+
parent.onTextChange(String(proposed.prefix(limit)))
378+
return false
379+
}
380+
381+
let updated = nsCurrent.replacingCharacters(in: range, with: digitsOnly)
318382
return updated.count <= 1
319383
}
320384
}

0 commit comments

Comments
 (0)