Lektion 2.10 · Stufe 2 – SwiftUI-Einstieg

@Binding: Zustand teilen

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du mit @Binding einer Kind-View erlauben, den Zustand ihrer Eltern-View zu lesen und zu ändern. Du verstehst den Unterschied zwischen @State und @Binding, das Prinzip der einzigen Quelle der Wahrheit und wie du Views mit Bindings in Previews testest.

Das Problem: Zustand in der falschen View

Aus Lektion 2.9 kennst du @State. Jetzt willst du eine Einstellungsseite bauen, mit einem eigenen, wiederverwendbaren Schalter. Naheliegend wäre:

Swift
struct SettingsView: View {
    @State private var notificationsOn = false

    var body: some View {
        VStack {
            FancySwitch()
            Text(notificationsOn ? "Benachrichtigungen an" : "Benachrichtigungen aus")
        }
    }
}

struct FancySwitch: View {
    @State private var isOn = false

    var body: some View {
        Button(isOn ? "AN" : "AUS") {
            isOn.toggle()
        }
        .font(.headline)
        .padding()
        .background(isOn ? .green : .gray)
        .foregroundStyle(.white)
        .clipShape(.capsule)
    }
}

Tippst du auf den Schalter, wechselt er zwischen AN und AUS. Aber der Text darunter bleibt bei „Benachrichtigungen aus“. Warum?

Es gibt zwei unabhängige Werte: notificationsOn in der SettingsView und isOn im FancySwitch. Der Schalter ändert nur seinen eigenen. Die beiden wissen nichts voneinander.

Was wir brauchen: Der Schalter soll keinen eigenen Wert haben, sondern den Wert seiner Eltern-View mitbenutzen, lesen und ändern dürfen.

Die Lösung: @Binding

Swift
struct FancySwitch: View {
    @Binding var isOn: Bool      // 👈 statt @State

    var body: some View {
        Button(isOn ? "AN" : "AUS") {
            isOn.toggle()
        }
        .font(.headline)
        .padding()
        .background(isOn ? .green : .gray)
        .foregroundStyle(.white)
        .clipShape(.capsule)
    }
}

Und in der Eltern-View übergibst du den Zustand mit $:

Swift
struct SettingsView: View {
    @State private var notificationsOn = false

    var body: some View {
        VStack(spacing: 20) {
            FancySwitch(isOn: $notificationsOn)   // 👈 mit $
            Text(notificationsOn ? "Benachrichtigungen an" : "Benachrichtigungen aus")
        }
    }
}

Jetzt ändert sich der Text mit dem Schalter.

Erwartetes Ergebnis

Ein grauer Knopf „AUS“ und darunter „Benachrichtigungen aus“. Nach einem Tipp: grüner Knopf „AN“ und „Benachrichtigungen an“.

Was @Binding bedeutet

Ein Binding ist eine Verbindung zu einem Wert, der woanders gespeichert ist. Es ist kein eigener Speicher, sondern eher eine Fernbedienung.

Ein Bild für Bindings

@State ist der Fernseher: Er gehört der Eltern-View, steht bei ihr im Wohnzimmer. @Binding ist die Fernbedienung: Die Kind-View bekommt sie in die Hand gedrückt. Sie kann damit umschalten und sieht, was läuft, aber der Fernseher steht nicht bei ihr. Und alle, die auf denselben Fernseher schauen, sehen sofort das neue Programm.

So spielen die Teile zusammen:

Text
SettingsView
  @State notificationsOn  ◀── der eigentliche Wert, gehört SettingsView
        │
        │ $notificationsOn  (Verbindung übergeben)
        ▼
FancySwitch
  @Binding isOn           ◀── liest und schreibt notificationsOn

Drückt der Nutzer auf den Schalter:

  1. isOn.toggle() im FancySwitch schreibt über die Verbindung in notificationsOn.
  2. SwiftUI bemerkt die Änderung an notificationsOn.
  3. SettingsView und FancySwitch werden neu berechnet.
  4. Schalter und Text zeigen den neuen Zustand.

$ erklärt

In Lektion 2.9 hattest du $ schon als Vorgeschmack gesehen. Jetzt verstehst du es ganz:

SchreibweiseTypBedeutung
notificationsOnBoolder aktuelle Wert
$notificationsOnBinding<Bool>eine Verbindung zum Wert

Mit $ holst du aus einem @State die Verbindung heraus, die du dann weitergibst. Innerhalb der Kind-View verwendest du die Binding-Eigenschaft wieder ganz normal ohne $: isOn liest, isOn = true schreibt.

Und weil eine Binding-Eigenschaft selbst wieder eine Verbindung ist, kannst du sie mit $ weiterreichen, beliebig tief:

Swift
struct SettingsRow: View {
    @Binding var isOn: Bool

    var body: some View {
        HStack {
            Text("Benachrichtigungen")
            Spacer()
            FancySwitch(isOn: $isOn)   // Verbindung weiterreichen
        }
    }
}

Apples Views benutzen Bindings auch

Jetzt ergibt etwas Sinn, was du schon gesehen hast:

Swift
Toggle("WLAN", isOn: $wifiOn)
TextField("Name", text: $name)
Slider(value: $volume)

Toggle, TextField und Slider funktionieren genau wie dein FancySwitch: Sie besitzen keinen eigenen Wert, sondern bekommen eine Verbindung zu deinem Zustand. Wenn der Nutzer sie bedient, ändern sie deinen Wert. Deshalb steht dort immer ein $.

Die einzige Quelle der Wahrheit

Das Prinzip hinter @Binding hat einen Namen: Single Source of Truth, die einzige Quelle der Wahrheit. Jede Information wird an genau einer Stelle gespeichert. Alle anderen Stellen lesen von dort oder bekommen eine Verbindung dorthin.

Im kaputten Beispiel am Anfang gab es zwei Quellen, notificationsOn und isOn, und sie liefen auseinander. Mit @Binding gibt es nur noch eine.

Die Grundfrage: Wem gehört der Wert?

Bevor du einen Zustand anlegst, frag dich: Welche View ist für diesen Wert zuständig? Das ist meist die höchste View, die ihn braucht. Dort steht @State. Alle Views darunter, die ihn ändern müssen, bekommen ein @Binding. Views, die ihn nur anzeigen, bekommen eine ganz normale Eigenschaft mit let.

Die View …verwendet
besitzt den Wert@State private var
darf den Wert lesen und ändern@Binding var
darf den Wert nur lesenlet

Bindings in Previews

Eine View mit @Binding kann ihren Wert nicht selbst erzeugen. In der Preview brauchst du deshalb einen Wert, an den du sie binden kannst. Zwei Möglichkeiten:

Konstante Verbindung, wenn du nur das Aussehen prüfen willst:

Swift
#Preview("An") {
    FancySwitch(isOn: .constant(true))
}

.constant(true) ist ein Binding, das immer true liefert und Änderungen ignoriert. Tippen bewirkt dann nichts.

Echter Zustand, wenn du interagieren willst (Lektion 2.3):

Swift
#Preview("Interaktiv") {
    @Previewable @State var isOn = false
    FancySwitch(isOn: $isOn)
}

Typische Fehler

$ vergessen

Swift
FancySwitch(isOn: notificationsOn)

Fehlermeldung

Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>'

Die View will eine Verbindung, du gibst ihr nur den Wert. Schreib $notificationsOn.

$ vor einer normalen Eigenschaft

Swift
let title = "Hallo"
TextField("Titel", text: $title)

Fehlermeldung

Cannot find '$title' in scope

$ funktioniert nur bei @State, @Binding und anderen Property Wrappern, die eine Verbindung anbieten. Eine let-Konstante kann man nicht ändern, also gibt es auch keine Verbindung.

@Binding mit Startwert

Swift
@Binding var isOn = false

Ein Binding hat keinen eigenen Wert, also auch keinen Startwert. Der Wert kommt immer von außen. Lösch das = false.

@Binding als private

Swift
@Binding private var isOn: Bool

Dann kann die Eltern-View die Verbindung nicht übergeben, und der automatische Initializer verlangt sie trotzdem. Bindings sind nicht private. Im Gegensatz zu @State, das immer private sein sollte.

Zwei Quellen der Wahrheit

Kein Compiler-Fehler, aber ein Logikfehler: Eine Kind-View hat ein eigenes @State, obwohl die Eltern-View denselben Wert braucht. Die Anzeige läuft auseinander. Lösung: @State in der Eltern-View, @Binding im Kind.

FAQ

Anfänger

Was bedeutet das überhaupt?

Wann @State, wann @Binding? @State, wenn die View den Wert besitzt, also dafür zuständig ist. @Binding, wenn sie einen fremden Wert ändern soll. Wenn sie ihn nur anzeigt, reicht let.

Kann ich ein Binding an mehrere Kind-Views geben? Ja, so oft du willst. Alle teilen sich dann denselben Wert, und eine Änderung in einer ist sofort in allen sichtbar.

Warum nicht einfach überall @State? Weil dann jede View ihren eigenen Wert hätte und sie auseinanderlaufen, wie im Beispiel am Anfang. @State gehört an eine Stelle.

Fortgeschritten

Warum verhält es sich so?

Kann ich ein Binding zu einer Eigenschaft einer Struct übergeben? Ja. Ist settings ein @State vom Typ Settings mit einer Eigenschaft isMuted, liefert $settings.isMuted ein Binding<Bool> direkt auf diese eine Eigenschaft. Das ist extrem praktisch für Formulare.

Kann ich ein Binding zu einem Array-Element übergeben? Ja, mit ForEach($items) { $item in ... } bekommst du für jedes Element eine eigene Verbindung. Das lernst du in Lektion 2.15 und nutzt es in Projekt C.

Wie reagiere ich auf eine Änderung in einem Binding? Mit .onChange(of: isOn) { oldValue, newValue in ... }, wie bei jedem anderen Wert (Lektion 2.2).

Profi

Was passiert unter der Haube?

Wie ist ein Binding aufgebaut? Im Kern ist ein Binding<Value> ein Paar aus zwei Closures: einem Getter, der den aktuellen Wert liefert, und einem Setter, der einen neuen Wert schreibt. Du kannst sogar eigene Bindings bauen: Binding(get: { ... }, set: { ... }). Das ist nützlich, um Werte beim Lesen oder Schreiben umzuwandeln. Setz es aber sparsam ein, weil es schwerer nachzuvollziehen ist.

Warum funktioniert $settings.isMuted? Binding unterstützt Dynamic Member Lookup über Key Paths: Aus einem Binding auf eine Struct wird über .isMuted automatisch ein Binding auf diese Eigenschaft. Getter und Setter werden dabei so verkettet, dass ein Schreiben die ganze Struct im ursprünglichen @State aktualisiert.

Übungen

Anfänger

Übung 1: Zähler-Knöpfe

Baue eine View CounterButtons mit @Binding var count: Int und zwei Buttons „−“ und „+“. Verwende sie in einer Eltern-View, die die Zahl groß anzeigt und den Zustand besitzt.

Lösung Übung 1Erst selbst probieren!
Swift
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("\(count)")
                .font(.system(size: 64, weight: .bold))
            CounterButtons(count: $count)
        }
    }
}

struct CounterButtons: View {
    @Binding var count: Int

    var body: some View {
        HStack(spacing: 20) {
            Button("−") { count -= 1 }
            Button("+") { count += 1 }
        }
        .font(.largeTitle)
    }
}

#Preview {
    CounterView()
}
Fortgeschritten

Übung 2: Zwei Schalter, ein Wert

Zeig in einer View zwei FancySwitch-Instanzen, die beide an denselben Wert gebunden sind. Was passiert, wenn du auf einen der beiden tippst? Und was passiert, wenn du einen davon mit .constant(false) verbindest?

Profi

Übung 3: Bewertung mit Sternen

Baue eine View StarRating mit @Binding var rating: Int. Sie zeigt fünf Sterne nebeneinander. Sterne bis zur aktuellen Bewertung sind gefüllt und gelb, die restlichen leer und grau. Ein Tipp auf einen Stern setzt die Bewertung auf seine Position. Verwende sie in einer Eltern-View, die zusätzlich „3 von 5 Sternen“ anzeigt.

Tipp: ForEach(1...5, id: \.self) { number in ... } erzeugt fünf Views. Mehr dazu in Lektion 2.15.

Lösung Übung 3Erst selbst probieren!
Swift
struct StarRating: View {
    @Binding var rating: Int

    var body: some View {
        HStack {
            ForEach(1...5, id: \.self) { number in
                Button {
                    rating = number
                } label: {
                    Image(systemName: number <= rating ? "star.fill" : "star")
                        .foregroundStyle(number <= rating ? .yellow : .gray)
                }
            }
        }
        .font(.title)
    }
}

struct ReviewView: View {
    @State private var rating = 3

    var body: some View {
        VStack(spacing: 12) {
            StarRating(rating: $rating)
            Text("\(rating) von 5 Sternen")
        }
    }
}

#Preview {
    ReviewView()
}

Der Button mit label: zeigt statt eines Textes eine beliebige View an. Das lernst du in der nächsten Lektion genauer.

Zusammenfassung

  • @Binding ist eine Verbindung zu einem Wert, der woanders gespeichert ist.
  • Die Eltern-View besitzt den Wert mit @State und übergibt ihn mit $.
  • Die Kind-View liest und schreibt ihn über @Binding var, ohne Startwert und nicht private.
  • Toggle, TextField, Slider und viele andere Views arbeiten mit Bindings.
  • Single Source of Truth: Jeder Wert hat genau eine Quelle.
  • In Previews: .constant(...) oder @Previewable @State.

Spickzettel

Swift
// Eltern-View
@State private var isOn = false
ChildView(isOn: $isOn)

// Kind-View
struct ChildView: View {
    @Binding var isOn: Bool
    var body: some View {
        Button("Umschalten") { isOn.toggle() }
    }
}

#Preview { ChildView(isOn: .constant(true)) }

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.