25. Oktober 2019

SwiftUI Tutorial: Bindings

Das Tutorial führt in die Verwendung von Bindings und dem @Binding-Property-Wrapper in SwiftUI ein.

Sofern Du SwiftUI noch nicht verwendet hast, empfehle ich zuvor das Tutorial: Einführung in SwiftUI und Verwendung von @State durchzuarbeiten.

  1. Lade für dieses Tutorial den Start-Stand von dem Projekt Shapes. Mit diesem kannst Du anhand eines praktischen Beispiels direkt die Verwendung von Bindings ausprobieren.

  2. Mache Dich mit dem Projekt vertraut. Es enthält einen Datentyp Circle zur Berechnung von Radius, Durchmesser und Fläche von einem Kreis und ein ShapeView, welches diese Werte anzeigt. Für die Darstellung eines Feldes wurde ein separates View DecimalView extrahiert:

    Beispiel-App Shapes
  3. Füge in DecimalView zusätzlich zu den Text-Labels ein TextField hinzu. Verwende die vorbereitete numberFormatter-Eigenschaft für den formatter-Parameter und den textFieldStyle-Modifier, um das Textfeld mit Rahmen anzuzeigen:

    struct DecimalView: View {
    
        var caption: String
        var value: Decimal
    
        var body: some View {
            VStack(alignment: .leading, spacing: 5) {
                Text(caption + ":")
                Text(self.numberFormatter.string(for: value) ?? "")
                TextField(caption, value: self.value, formatter: self.numberFormatter)
        .textFieldStyle(RoundedBorderTextFieldStyle())
            }
        }
    
        var numberFormatter: Formatter = {
            let formatter = NumberFormatter()
            formatter.numberStyle = .decimal
            formatter.generatesDecimalNumbers = true
            return formatter
        }()
    }
  4. Dies führt zu einem Fehler, da für value ein Binding erwartet wird. Ein Binding erlaubt SwiftUI den Wert zu holen als auch bei Änderungen zu setzen. Der Fehler wird leider nicht direkt angezeigt- siehe Umgang mit Fehlermeldungen in SwiftUI.

    Behebe dieses Problem, indem Du die Eigenschaft value als @State deklarierst (dieser Property Wrapper macht die Eigenschaft veränderbar). Eine solche Eigenschaft stellt als sog. projected value (beginnend mit $-Zeichen) ein Binding für die Eigenschaft bereit - verwende diese für den value des Textfeldes:

    struct DecimalView: View {
    
        var caption: String
        @State var value: Decimal
    
        var body: some View {
            VStack(alignment: HorizontalAlignment.leading, spacing: 5) {
                Text(caption + ":")
                Text(self.numberFormatter.string(for: value) ?? "")
                TextField(caption, value: self.$value, formatter: self.numberFormatter)
            }
        }
    
        // …
    }
  5. Starte die App und teste das Verhalten. Wird in einem Textfeld getippt und mit Enter bestätigt, wird der Wert innerhalb von DecimalView übernommen. Dies ist sichtbar an dem noch vorhandenen Label: es wird aktualisiert weil die Änderung an der @State-Eigenschaft die Aktualisierung des Views auslöst. Der Wert wird allerdings nicht an das übergeordnete ShapeView weitergegeben:

  6. Deklariere die DecimalView-Eigenschaft value als @Binding - damit wird der Wert nicht mehr im View gehalten sondern ein externer Wert per Binding referenziert:

    struct DecimalView: View {
    
        var caption: String
        @Binding var value: Decimal
    
        // …
    
    }
  7. Verwende eine @State-Eigenschaft im PreviewProvider, um dem View für die Vorschau ein Binding übergeben zu können:

    struct NumberView_Previews: PreviewProvider {
        
        @State static var value : Decimal = 5
        
        static var previews: some View {
            DecimalView(caption: "Area", value:  $value)
        }
    }
  8. Verwende im ShapeView für die circle-Eigenschaft @State um diese als View-Zustand zu deklarieren. Mit dem $-projected value kann nicht nur der Wert sondern auch dessen Eigenschaften als Binding referenziert werden:

    struct ShapeView: View {
    
        @State var circle = Circle(radius: 1)
    
        var body: some View {
    
            VStack(alignment: .leading, spacing: 10) {
                DecimalView(caption: "Radius", value: $circle.radius)
                DecimalView(caption: "Durchmesser", value: $circle.diameter)
                DecimalView(caption: "Fläche", value: $circle.area)
                Spacer()
            }.padding(10)
        }
    
    }
  9. Entferne das unnötige Label für den Wert in DecimalView und teste das Verhalten der App: