Skip to content

Instantly share code, notes, and snippets.

@imthath-m
Last active April 21, 2022 18:14
Show Gist options
  • Save imthath-m/27590dad5f2c78a59457497aeebdbdf9 to your computer and use it in GitHub Desktop.
Save imthath-m/27590dad5f2c78a59457497aeebdbdf9 to your computer and use it in GitHub Desktop.
import SwiftUI
// import Introspect
public struct PasscodeField: View {
var maxDigits: Int = 4
var label = "Enter One Time Password"
@State var pin: String = ""
@State var showPin = false
@State var isDisabled = false
var handler: (String, (Bool) -> Void) -> Void
public var body: some View {
VStack(spacing: .large) {
Text(label).font(.title)
ZStack {
pinDots
backgroundField
}
showPinStack
}
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<maxDigits) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: .large, weight: .thin, design: .default))
Spacer()
}
}
}
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)
// Introspect library can used to make the textField become first resonder on appearing
// if you decide to add the pod 'Introspect' and import it, comment #50 to #53 and uncomment #55 to #61
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
.disabled(isDisabled)
// .introspectTextField { textField in
// textField.tintColor = .clear
// textField.textColor = .clear
// textField.keyboardType = .numberPad
// textField.becomeFirstResponder()
// textField.isEnabled = !self.isDisabled
// }
}
private var showPinStack: some View {
HStack {
Spacer()
if !pin.isEmpty {
showPinButton
}
}
.frame(height: .averageTouchSize)
.padding([.trailing])
}
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() {
guard !pin.isEmpty else {
showPin = false
return
}
if pin.count == maxDigits {
isDisabled = true
handler(pin) { isSuccess in
if isSuccess {
print("pin matched, go to next page, no action to perfrom here")
} else {
pin = ""
isDisabled = false
print("this has to called after showing toast why is the failure")
}
}
}
// this code is never reached under normal circumstances. If the user pastes a text with count higher than the
// max digits, we remove the additional characters and make a recursive call.
if pin.count > maxDigits {
pin = String(pin.prefix(maxDigits))
submitPin()
}
}
private func getImageName(at index: Int) -> String {
if index >= self.pin.count {
return "circle"
}
if self.showPin {
return self.pin.digits[index].numberString + ".circle"
}
return "circle.fill"
}
}
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)
}
}
@jahnaviwisdom
Copy link

how can i use this PasscodeField ?

@imthath-m
Copy link
Author

PasscodeField { otp, completionHandler in
            // check if the otp is correct here
            if isCorrect(otp) { // this could be a network call 
                completionHandler(true)
            } else {
                completionHandler(false)
            }
        }

For more details, read this article and the comments.

@eliasayele
Copy link

is Introspect library is a must? the code doesn't work

@tnmendes
Copy link

You are missing the "@escaping"

var handler: (String, @escaping(Bool) -> Void) -> Void

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment