I have been looking for a while for a way to create a pin field in iOS 15 with SwiftUI. So a field where you just type in numbers and they are displayed. If 6 numbers are entered, they are checked and a corresponding action is performed. However, only numbers should be allowed and the individual text fields must be separated, because I want to format the individual numbers individually.
After much back and forth, I have now found a way to do this. If someone finds a better way, I would be happy to see it. If someone finds my idea good, feel free to use it.
So here is the code.
// add the function removeAllNonNumeric to stings
// later you can use removeAllNonNumeric() on strings to remove all non numbers
extension RangeReplaceableCollection where Self: StringProtocol {
mutating func removeAllNonNumeric() {
removeAll { !$0.isWholeNumber }
}
}
// holds one single pin element
struct PinElementView: View {
let pin: String
let index: Int
init(pin: String, index: Int) {
self.index = index
self.pin = pin.count > index ? String(Array(pin)[index]) : ""
}
var body: some View {
Text(pin)
.frame(width: 20, height: 25)
.overlay(
RoundedRectangle(cornerRadius: 3)
.stroke(Color.gray, lineWidth: 2))
}
}
// holds the pin input view
struct PinInputView: View {
// focus the hidden text field
@FocusState private var isFocused: Bool
// the whole pin is saved as state in here
@State var pin: String = ""
@State private var showPopover: Bool = false
var body: some View {
ZStack {
TextField("", text: Binding(
get: { pin },
set: { newValue in
pin = newValue
pin.removeAllNonNumeric()
// if there are 6 numbers, show the pin and reset it
if pin.count == 6 {
print("The input pin value is \(pin)")
self.showPopover = true
}
}
))
.focused($isFocused)
.keyboardType(.numberPad)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
isFocused = true
}
}
.frame(width: 100, height: 50)
// lay an white frame over the text field to hide it
// hidden() on the textfield results in a state, that you are not able to input values
Rectangle().fill(.white).frame(width: 100, height: 50)
// the pin fields where the pin is shown later
HStack {
ForEach(0 ... 5, id: \.self) { index in
PinElementView(pin: pin, index: index)
}
}
.onTapGesture {
isFocused = true
}
}
.actionSheet(isPresented: $showPopover) {
// show the pin and remove it afterwards
ActionSheet(
title: Text("Input"),
message: Text("You typed the pin \(pin)"),
buttons: [
.default(Text("OK")) { pin = "" },
]
)
}
}
}
The result will look like this:
If you have tiped the 6 digits, there will be an action sheet and it will reset the pin afterwards.