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:

The result of the code

If you have tiped the 6 digits, there will be an action sheet and it will reset the pin afterwards.