Lernziel
Nach dieser Lektion kannst du Views bauen, die sich verändern: Zähler, Schalter, ein- und ausklappbare Bereiche. Du weißt, was @State macht, warum du es brauchst und wann du es einsetzt.
Das Problem: Eine View, die sich nicht ändern will
Bisher waren alle unsere Views statisch. Die Visitenkarte aus Projekt B sieht immer gleich aus, egal wie oft du darauf tippst. Echte Apps reagieren aber: Ein Like-Button wird rot, ein Zähler zählt hoch, ein Menü klappt auf.
Bauen wir also einen einfachen Zähler. Mit dem, was du aus Stufe 1 weißt, würdest du es wahrscheinlich so versuchen:
struct CounterView: View {
var count = 0
var body: some View {
VStack(spacing: 20) {
Text("\(count)")
.font(.largeTitle)
Button("Plus eins") {
count += 1
}
}
}
}Das sieht vernünftig aus. Xcode ist aber anderer Meinung:
Fehlermeldung
Left side of mutating operator isn't mutable: 'self' is immutable
Übersetzt: „Du willst etwas verändern, das nicht verändert werden darf.“
Warum? Erinnere dich an Lektion 1.12: Views sind Structs. Innerhalb einer Struct darf eine normale Funktion oder Closure die eigenen Eigenschaften nicht verändern. Und selbst wenn das ginge, gäbe es ein zweites, größeres Problem. Dazu gleich mehr.
Die Lösung: @State
Ändere eine einzige Zeile:
struct CounterView: View {
@State private var count = 0 // 👈 das ist neu
var body: some View {
VStack(spacing: 20) {
Text("\(count)")
.font(.largeTitle)
Button("Plus eins") {
count += 1
}
}
}
}
#Preview {
CounterView()
}Der Fehler ist weg. Tippst du in der Preview auf den Button, zählt die Zahl hoch.
Erwartetes Ergebnis
In der Mitte des Bildschirms steht groß eine „0“, darunter ein blauer Button „Plus eins“. Jeder Tipp erhöht die Zahl um 1.
Damit kennst du @State. Jetzt schauen wir uns an, was dahinter steckt, denn hier liegt der Schlüssel zum ganzen SwiftUI-Denken.
Was @State wirklich macht
Views sind Baupläne, keine Objekte
In SwiftUI ist eine View kein dauerhaftes Ding auf dem Bildschirm. Sie ist eher ein Bauplan: „So soll es aussehen.“ SwiftUI liest diesen Bauplan, zeichnet den Bildschirm danach und wirft den Bauplan wieder weg.
Ändert sich etwas, ruft SwiftUI body erneut auf, bekommt einen neuen Bauplan und passt den Bildschirm an. Das passiert ständig und sehr schnell.
Genau das ist das zweite Problem von vorhin: Wäre count eine normale Eigenschaft, würde sie bei jedem neuen Bauplan wieder bei 0 beginnen. Die View hätte kein Gedächtnis.
@State ist das Gedächtnis
Mit @State sagst du zu SwiftUI:
„Bewahre diesen Wert für mich auf, außerhalb der View. Und wenn er sich ändert, zeichne die View neu.“
SwiftUI legt den Wert also in einem eigenen Speicher ab, der so lange lebt, wie die View auf dem Bildschirm ist. Deine View bekommt nur Zugriff darauf.
Der Ablauf beim Tippen auf den Button:
1. Du tippst auf „Plus eins“
2. count += 1 → der Wert im SwiftUI-Speicher wird 1
3. SwiftUI merkt: „count hat sich geändert!“
4. SwiftUI ruft body erneut auf
5. body liefert Text("1") → der Bildschirm zeigt 1Du sagst SwiftUI also nie: „Ändere den Text auf 1.“ Du änderst nur die Daten, und SwiftUI kümmert sich um den Rest.
Die wichtigste Formel in SwiftUI
UI = f(State)
Die Oberfläche ist eine Funktion des Zustands.
Bei gleichem Zustand sieht die Oberfläche immer gleich aus. Willst du, dass sich etwas auf dem Bildschirm ändert, änderst du den Zustand. Diesen Gedanken wirst du ab jetzt in jeder Lektion wiedersehen.
Zweites Beispiel: Etwas ein- und ausblenden
@State funktioniert mit jedem Typ, nicht nur mit Zahlen. Ein Bool eignet sich perfekt zum Ein- und Ausklappen:
struct RecipeCard: View {
@State private var showsDetails = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Spaghetti Carbonara")
.font(.title2.bold())
if showsDetails {
Text("Zutaten: Spaghetti, Eier, Pecorino, Guanciale, Pfeffer.")
.foregroundStyle(.secondary)
}
Button(showsDetails ? "Weniger anzeigen" : "Mehr anzeigen") {
showsDetails.toggle()
}
}
.padding()
}
}
#Preview {
RecipeCard()
}Hier sind zwei Dinge neu:
ifdirekt inbody: Du beschreibst einfach, wann der Text da sein soll. IstshowsDetailstrue, gehört der Text zum Bauplan, sonst nicht..toggle(): kehrt einenBoolum. Austruewirdfalseund umgekehrt.
Auch der Button-Titel hängt vom Zustand ab. Eine einzige Variable steuert also zwei Stellen der Oberfläche, und beide passen immer zusammen. In der imperativen Programmierung müsstest du beide Stellen getrennt aktualisieren und könntest eine davon vergessen.
Erwartetes Ergebnis
Zuerst nur Titel und „Mehr anzeigen“. Nach dem Tippen erscheinen die Zutaten, und der Button heißt „Weniger anzeigen“.
Drittes Beispiel: Ein kleiner Vorgeschmack auf $
Manche Bedienelemente wollen den Zustand nicht nur lesen, sondern selbst verändern, zum Beispiel ein Textfeld, in das jemand tippt:
struct GreetingView: View {
@State private var name = ""
var body: some View {
VStack(spacing: 16) {
TextField("Dein Name", text: $name)
.textFieldStyle(.roundedBorder)
Text(name.isEmpty ? "Hallo!" : "Hallo, \(name)!")
.font(.title)
}
.padding()
}
}Beachte das $ vor name. Es bedeutet: „Ich gebe dir nicht den Wert, sondern eine Verbindung zum Wert. Du darfst ihn ändern.“ Diese Verbindung heißt Binding, und ihr gehört die ganze nächste Lektion.
Für jetzt reicht die Faustregel:
| Schreibweise | Bedeutung | Beispiel |
|---|---|---|
name | Wert lesen | Text(name) |
$name | Wert lesen und schreiben lassen | TextField("…", text: $name) |
Die vier Regeln für @State
1. Immer private
@State gehört der View, in der es steht. Keine andere View soll es von außen setzen. Mit private macht Xcode das sogar für dich sichtbar.
2. Immer mit Startwert
@State private var count = 0. SwiftUI braucht einen Wert, mit dem es beginnen kann.
3. Für einfache, lokale Daten
Zahlen, Texte, Bools, kleine Structs, Arrays: alles, was nur diese View (und ihre Kinder) betrifft. Ob ein Menü offen ist, was gerade im Suchfeld steht, welcher Tab ausgewählt ist.
4. Nur in Views verwenden
@State funktioniert ausschließlich innerhalb von SwiftUI-Views. In einer normalen Struct oder Klasse hat es keine Wirkung.
Typische Fehler
Fehler 1: @State vergessen
Fehlermeldung
Left side of mutating operator isn't mutable: 'self' is immutable
Du versuchst, eine normale Eigenschaft zu verändern. Lösung: @State davorschreiben.
Fehler 2: Zustand in body ändern
var body: some View {
count += 1 // ❌ Niemals!
return Text("\(count)")
}Laufzeitwarnung
Modifying state during view update, this will cause undefined behavior.
body soll nur beschreiben, nicht verändern. Eine Änderung in body löst ein neues body aus, das wieder ändert, und so weiter. Ändere den Zustand nur als Reaktion auf etwas: in einem Button, einer Geste oder in .onAppear.
Fehler 3: @State von außen befüllen wollen
struct ScoreView: View {
@State var score: Int // ❌ nicht private, kein Startwert
...
}
ScoreView(score: 10)Das kompiliert sogar, führt aber zu einem verwirrenden Verhalten: SwiftUI übernimmt den Wert nur beim allerersten Mal. Ändert sich der Wert von außen später, merkt die View davon nichts. Kommen die Daten von außen, ist das ein Fall für @Binding (nächste Lektion) oder eine ganz normale let-Eigenschaft.
Fehler 4: $ vergessen
Fehlermeldung
Cannot convert value of type 'String' to expected argument type 'Binding<String>'
Das Textfeld will eine Verbindung, du gibst ihm aber nur den Wert. Lösung: text: $name statt text: name.
FAQ
Anfänger
Was bedeutet das überhaupt?Was ist dieses @ überhaupt?
Ein @ vor einer Eigenschaft markiert einen Property Wrapper, eine Art Hülle, die der Eigenschaft Zusatzfähigkeiten gibt. @State gibt ihr ein Gedächtnis und die Fähigkeit, die View neu zeichnen zu lassen. Weitere Property Wrapper lernst du bald kennen, zum Beispiel @Binding und @Environment.
Wird die ganze App neu gezeichnet, wenn sich count ändert?
Nein. Nur die View, die count besitzt, und ihre Kind-Views werden neu berechnet. Und selbst dann ändert SwiftUI auf dem Bildschirm nur das, was wirklich anders aussieht.
Ist das nicht langsam, ständig body aufzurufen?
Nein. Views sind kleine, leichte Structs, und SwiftUI ist genau dafür optimiert. Wichtig ist nur, dass body keine aufwendigen Berechnungen enthält. Dazu mehr in Lektion 4.5.
Warum ist der Zähler wieder bei 0, wenn ich die App neu starte?
@State lebt nur, solange die View existiert. Für dauerhaftes Speichern gibt es @AppStorage (Lektion 3.13) und SwiftData (Lektion 3.14).
Fortgeschritten
Warum verhält es sich so?Wann @State, wann nicht?
Faustregel: Gehört der Wert nur dieser View und betrifft er nur die Oberfläche (offen/zu, Eingabetext, Auswahl), dann @State. Brauchen mehrere Views dieselben Daten oder steckt Logik dahinter, wandern sie in eine @Observable-Klasse (Lektion 3.1).
Kann ich auch eigene Structs in @State speichern?
Ja. Ändert sich irgendeine Eigenschaft der Struct, gilt der ganze Wert als geändert, und die View wird neu gezeichnet:
struct Settings {
var volume = 0.5
var isMuted = false
}
@State private var settings = Settings()
// settings.isMuted = true → View wird aktualisiertWarum wird mein @State manchmal zurückgesetzt?
Weil SwiftUI die View als neue View betrachtet, etwa weil sie in einem if verschwunden und wieder aufgetaucht ist. Dann bekommt sie frischen Speicher mit dem Startwert. Das Thema heißt View-Identität und wird in Lektion 4.1 ausführlich erklärt.
Wie benutze ich @State in einer Preview?
Mit dem Makro @Previewable, das eine Preview mit eigenem Zustand ermöglicht:
#Preview {
@Previewable @State var name = "Anna"
TextField("Name", text: $name)
}Profi
Was passiert unter der Haube?Wo liegt der Wert wirklich?
Nicht in der Struct. @State enthält nur einen Verweis auf einen Speicherplatz, den SwiftUI im sogenannten Attribut-Graphen verwaltet und an die Identität der View im View-Baum bindet. Die Struct selbst kann beliebig oft neu erzeugt werden, der Speicher bleibt bestehen, solange die Identität gleich bleibt.
Warum wird der Startwert nur einmal übernommen?
Der Ausdruck = 0 wird bei jeder Erzeugung der Struct ausgewertet. SwiftUI nutzt ihn aber nur, wenn für diese Identität noch kein Speicher existiert. Alle späteren Werte werden ignoriert. Deshalb funktioniert Fehler 3 nicht wie erwartet.
Was macht $ technisch?
$count greift auf die projectedValue-Eigenschaft des Property Wrappers zu. Bei @State ist das ein Binding<Int>, also ein Paar aus Getter und Setter, das auf denselben Speicher zeigt.
Kann ich auch Klassen in @State speichern?
Ja, und das ist bei @Observable-Klassen sogar der empfohlene Weg, damit die View das Objekt besitzt (Lektion 3.2). Wichtig: Bei Klassen beobachtet @State selbst nur, ob die Referenz ausgetauscht wird. Änderungen an Eigenschaften erkennt SwiftUI nur über @Observable.
Übungen
Übung 1: Rückwärts zählen
Erweitere den Zähler um einen zweiten Button „Minus eins“. Der Zähler soll nie unter 0 fallen.
Lösung Übung 1Erst selbst probieren!
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("\(count)")
.font(.largeTitle)
HStack(spacing: 20) {
Button("Minus eins") {
if count > 0 {
count -= 1
}
}
Button("Plus eins") {
count += 1
}
}
}
}
}Bonus: Statt der if-Abfrage kannst du den Button mit .disabled(count == 0) deaktivieren. Dann sieht der Nutzer sofort, dass es nicht weiter nach unten geht.
Übung 2: Lichtschalter
Baue eine View mit einem großen Glühbirnen-Symbol (lightbulb / lightbulb.fill). Ein Tipp auf den Button schaltet das Licht an und aus. Ist es an, wird das Symbol gelb und der Hintergrund hell, sonst grau und dunkel.
Lösung Übung 2Erst selbst probieren!
struct LightSwitchView: View {
@State private var isOn = false
var body: some View {
VStack(spacing: 40) {
Image(systemName: isOn ? "lightbulb.fill" : "lightbulb")
.font(.system(size: 120))
.foregroundStyle(isOn ? .yellow : .gray)
Button(isOn ? "Ausschalten" : "Einschalten") {
isOn.toggle()
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(isOn ? Color.white : Color.black)
}
}Übung 3: Würfel-App
Ein Button „Würfeln“ zeigt eine Zufallszahl von 1 bis 6 als SF Symbol (die.face.1 … die.face.6). Darunter steht, wie oft schon gewürfelt wurde und wie oft eine 6 kam.
Tipp: Int.random(in: 1...6)
Lösung Übung 3Erst selbst probieren!
struct DiceView: View {
@State private var value = 1
@State private var rolls = 0
@State private var sixes = 0
var body: some View {
VStack(spacing: 30) {
Image(systemName: "die.face.\(value)")
.font(.system(size: 120))
Button("Würfeln") {
value = Int.random(in: 1...6)
rolls += 1
if value == 6 {
sixes += 1
}
}
.buttonStyle(.borderedProminent)
Text("Würfe: \(rolls) · Sechser: \(sixes)")
.foregroundStyle(.secondary)
}
}
}Profi-Gedanke: Drei einzelne @State-Werte gehören inhaltlich zusammen. Wie würdest du sie in einer Struct DiceStats bündeln?
Zusammenfassung
- Views sind Baupläne. SwiftUI erzeugt sie ständig neu.
- Normale Eigenschaften haben deshalb kein Gedächtnis.
@Statespeichert einen Wert außerhalb der View und zeichnet die View neu, wenn er sich ändert.- UI = f(State): Du änderst Daten, nicht die Oberfläche.
$namegibt eine Verbindung (Binding) weiter, mit der andere den Wert ändern dürfen.@Stateist immerprivate, hat einen Startwert und ist für lokale Daten gedacht.
Spickzettel
@State private var count = 0 // Zustand anlegen
Text("\(count)") // lesen
Button("+") { count += 1 } // ändern (nur in Reaktionen!)
Toggle("An", isOn: $isOn) // Binding weitergeben
showsDetails.toggle() // Bool umkehren