I tried implementing it with six textfields but found a number of problems as a lot of work, blocking all but the first textfield for initial input, laggy move of firstResponder and whatnot, what made me wonder if having 6 textfields is really the best approach.
The hard part is the functionality (i.e the cursor moving smoothly, getting back and forth, making all of them red when input is wrong, etc) How could I achieve such behaviour/functionality?
Screenshot:-
Code Below:-
import SwiftUI
struct VerficationCode: View {
@State private var numberOfCells: Int = 6
@State private var currentlySelectedCell = 0
var body: some View {
HStack {
Group {
ForEach(0 ..< self.numberOfCells) { index in
CharacterInputCell(currentlySelectedCell: self.$currentlySelectedCell, index: index)
}
}.frame(width:15,height: 56)
.padding(.horizontal)
.foregroundColor(.white)
.cornerRadius(10)
.keyboardType(.numberPad)
}
}
}
struct CharacterInputCell: View {
@State private var textValue: String = ""
@Binding var currentlySelectedCell: Int
var index: Int
var responder: Bool {
return index == currentlySelectedCell
}
var body: some View {
CustomTextField(text: $textValue, currentlySelectedCell: $currentlySelectedCell, isFirstResponder: responder)
}
}
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
@Binding var currentlySelectedCell: Int
var didBecomeFirstResponder = false
init(text: Binding<String>, currentlySelectedCell: Binding<Int>) {
_text = text
_currentlySelectedCell = currentlySelectedCell
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.text = textField.text ?? ""
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentText = textField.text ?? ""
guard let stringRange = Range(range, in: currentText) else { return false }
let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
if updatedText.count <= 1 {
self.currentlySelectedCell = 1
}
return updatedText.count <= 1
}
}
@Binding var text: String
@Binding var currentlySelectedCell: Int
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.keyboardType = .decimalPad
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text, currentlySelectedCell: $currentlySelectedCell)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
Can someone please explain to me How could I achieve such behaviour/functionality?, I've tried to implement but no results yet.
Any help would be greatly appreciated.
Thanks in advance.
CodePudding user response:
Code:-
import SwiftUI
public struct ContentView: View {
var maxDigits: Int = 6
var label = "Enter One Time Password"
@State var pin: String = ""
@State var showPin = true
var handler: (String, (Bool) -> Void) -> Void
public var body: some View {
VStack {
Text(label).font(.title)
ZStack {
pinDots
backgroundField
}
}
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<maxDigits) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 60, weight: .thin, design: .default))
Spacer()
}
}
}
private func getImageName(at index: Int) -> String {
if index >= self.pin.count {
return "square"
}
if self.showPin {
return self.pin.digits[index].numberString ".square"
}
return "square"
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { self.pin }, set: { newValue in
self.pin = newValue
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
}
private var showPinButton: some View {
Button(action: {
self.showPin.toggle()
}, label: {
self.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
if pin.count == maxDigits {
handler(pin) { isSuccess in
if isSuccess {
print("pin matched, go to next page, no action to perfrom here")
} else {
pin = ""
print("this has to called after showing toast why is the failure")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
//maxDigits = Set According to your condition
//label = Set Title
//Pin Count
ContentView(maxDigits: 6, label: "A", pin: "6", showPin: true) { strng, reponsetrue in
}
}
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}