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:
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
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 $:
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:
SettingsView
@State notificationsOn ◀── der eigentliche Wert, gehört SettingsView
│
│ $notificationsOn (Verbindung übergeben)
▼
FancySwitch
@Binding isOn ◀── liest und schreibt notificationsOnDrückt der Nutzer auf den Schalter:
isOn.toggle()imFancySwitchschreibt über die Verbindung innotificationsOn.- SwiftUI bemerkt die Änderung an
notificationsOn. SettingsViewundFancySwitchwerden neu berechnet.- Schalter und Text zeigen den neuen Zustand.
$ erklärt
In Lektion 2.9 hattest du $ schon als Vorgeschmack gesehen. Jetzt verstehst du es ganz:
| Schreibweise | Typ | Bedeutung |
|---|---|---|
notificationsOn | Bool | der aktuelle Wert |
$notificationsOn | Binding<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:
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:
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 lesen | let |
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:
#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):
#Preview("Interaktiv") {
@Previewable @State var isOn = false
FancySwitch(isOn: $isOn)
}Typische Fehler
$ vergessen
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
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
@Binding var isOn = falseEin Binding hat keinen eigenen Wert, also auch keinen Startwert. Der Wert kommt immer von außen. Lösch das = false.
@Binding als private
@Binding private var isOn: BoolDann 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
Ü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!
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()
}Ü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?
Ü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!
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
@Bindingist eine Verbindung zu einem Wert, der woanders gespeichert ist.- Die Eltern-View besitzt den Wert mit
@Stateund übergibt ihn mit$. - Die Kind-View liest und schreibt ihn über
@Binding var, ohne Startwert und nichtprivate. Toggle,TextField,Sliderund viele andere Views arbeiten mit Bindings.- Single Source of Truth: Jeder Wert hat genau eine Quelle.
- In Previews:
.constant(...)oder@Previewable @State.
Spickzettel
// 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)) }