Textfeld auf Basis von Zahlenwerten in SwiftUI (+ Nachtrag)

Aus der Community kam des Öfteren bereits die Frage, wie man in SwiftUI ein Textfeld umsetzen kann, das ausschließlich den Umgang mit Zahlenwerten erlaubt. Ein Beispiel für eine mögliche Lösung für dieses Szenario stelle ich euch in diesem Artikel vor.

Herzstück ist ein eigens definiertes Binding auf Basis eines Strings. Ein solches benötigen wir zwingend, da jede TextField-Instanz ein String-Binding voraussetzt.

Das von mir bereitgestellte String-Binding besitzt jedoch einen Kniff. Es liefert einen Zahlenwert auf Basis eines Intergers über seinen Getter zurück. Und über den Setter prüfe ich, ob sich die Eingabe des Nutzers in einen Integer umwandeln lässt. Falls ja, speichere ich jenen Integer im zugehörigen Status. Handelt es sich bei der Eingabe nicht um einen Zahlenwert, setze ich den Status auf den Wert 0 zurück. Der nachfolgende Code zeigt die Umsetzung dieser Lösung.

struct ContentView: View {
    @State private var number: Int = 0
    
    private var numberStringBinding: Binding<String> {
        Binding<String> {
            "\(number)"
        } set: {
            guard let newNumber = Int($0) else {
                number = 0
                return
            }
            number = newNumber
        }

    }
    
    var body: some View {
        VStack {
            Text("Number: \(number)")
            Divider()
            TextField("Number", text: numberStringBinding)
                .textFieldStyle(.roundedBorder)
                .keyboardType(.numberPad)
        }
        .padding()
    }
}

Um diese Umsetzung zu optimieren, bietet es sich an, als Keyboard-Type über den gleichnamigen Modifier die Option numberPad zu setzen. Auf dem iPhone kann der Nutzer über die virtuelle Bildschirmtastatur so tatsächlich nur Zahlen eingeben. Das schränkt die Möglichkeit für Fehleingaben sehr stark ein.

Ein Allheilmittel ist das Setzen des Keyboard-Types aber leider nicht: Auf dem iPad stehen mit dem numberPad-Style auch Buchstaben zur Eingabe zur Verfügung. Außerdem können Nutzer unabhängig vom Keyboard-Type ihr Device auch mit einer Bluetooth-Tastatur koppeln, wodurch die Beschränkung der Eingabe auf Zahlen wieder aufgehoben ist.

Ein String-Binding, so wie im oberen Code-Listing zu sehen, ist also unabdingbar, um ausschließlich Zahleneingaben zu verarbeiten.

Freilich ist die hier skizzierte Lösung noch nicht perfekt. Es ist beispielsweise nicht schön, dass bei Eingabe eines Buchstaben der Zahlenwert auf 0 zurückgesetzt wird. Aber zusammen mit dem numberPad-Keyboard-Type auf dem iPhone stellt diese verhältnismäßig einfache Lösung zumindest schon einmal eine gute (wenn auch noch ausbaufähige) Basis dar.

Wer mag, kann die gesamte Funktionalität in eine eigene View auslagern, um über sie numerische Textfelder in SwiftUI auf Basis eines Integer-Bindings zu erzeugen. Eine solche Implementierung könnte beispielsweise wie folgt aussehen:

struct NumericTextField: View {
    @Binding var number: Int
    
    private var numberStringBinding: Binding<String> {
        Binding<String> {
            "\(number)"
        } set: {
            guard let newNumber = Int($0) else {
                number = 0
                return
            }
            number = newNumber
        }

    }
    
    var body: some View {
        TextField("Number", text: numberStringBinding)
           .keyboardType(.numberPad)
    }
}

Ausgehend von dieser grundlegenden und essenziellen Anpassung kann man in weiteren Schritten zusätzliche Optimierungen an der Zahleneingabe durchführen. Doch man wird in SwiftUI nicht um den Einsatz eines eigenen String-Bindings herumkommen (idealerweise gekoppelt mit dem Einsatz des numerPad-Keyboard-Types).

Euer Thomas

Nachtrag: Zahleneingaben mittels Formatter verarbeiten

Dank euch, liebe Community, komme ich in den Genuss eines kleinen Nachtrags zu diesem Artikel. 🙂 Denn ihr habt mich zurecht darauf aufmerksam gemacht, dass sich auch ein alternativer TextField-Initializer in dem beschriebenen Szenario nutzen lässt.

So erlaubt es init(value:formatter:prompt:label:), ein Textfeld zu erzeugen, das ein Binding auf Grundlage eines beliebigen Typs (zum Beispiel Int) nutzt. Über den formatter-Parameter gibt ihr eine Formatter-Instanz mit, die bestimmt, wie der im Textfeld eingegebene String in den als Binding übergebenen Typ umzuwandeln ist. Im Falle von Zahlenwerten nutzt ihr zu diesem Zweck beispielsweise einen NumberFormatter.

Ein Beispiel zum Einsatz dieses Initializers zeigt das nachfolgende Listing. Der Code ersetzt das zuvor erzeugte NumericTextField und erfüllt denselben Zweck. Jedoch müsst ihr nun kein eigenes String-Binding mehr erzeugen, sondern nutzt den numberFormatter, um den eingegebenen String in eine passende Int-Instanz umzuwandeln. TextField kümmert sich um die zugehörige Logik.

struct NewNumericTextField: View {
    @Binding var number: Int
    
    private var numberFormatter: NumberFormatter {
        let numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        return numberFormatter
    }

    var body: some View {
        TextField(value: $number, formatter: numberFormatter) {
            Text("Number")
        }
    }
}

Ganz perfekt ist aber auch diese Lösung noch nicht: So ist es weiterhin möglich, jeden beliebigen Text innerhalb des Textfelds einzugeben. Kann aber TextField den eingegebenen String nicht mithilfe des definierten Formatters umwandeln, wird der Wert des Bindings schlicht nicht verändert (wohingegen wir im vorherigen Beispiel jenen Wert zurücksetzten).

Das als kleines Update zu diesem Artikel. 🙂 Seid mir bitte nicht böse, dass ich dieses alternative Vorgehen auf Basis des gezeigten Initializers verschwitzt habe; da merkt man, wie selten ich mich bisher mit der Zahleneingabe über Textfelder in SwiftUI befasst habe. 😉 Ich hatte diese Thematik schlicht nicht mehr auf dem Schirm.

Dafür habt ihr nun zwei Varianten zur Lösung dieses Problems gesehen: einmal auf Basis eines eigenen Bindings, und dann noch einmal mithilfe eines Formatters.

Neben der schlankeren Syntax hat die Formatter-Variante ebenfalls noch eine enorme Stärke: Sie kann mit allen möglichen Arten von Formattern umgehen. So könntet ihr – den passenden Formatter vorausgesetzt – beispielsweise auch Datumsangaben über ein Textfeld verarbeiten.


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert