Lektion 2.21 · Stufe 2 – SwiftUI-Einstieg

Views sinnvoll zerlegen

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du große Views in kleine, lesbare Bausteine zerlegen: mit eigenen Views, berechneten Eigenschaften und @ViewBuilder-Funktionen. Du weißt, wie du Daten und Aktionen an Unter-Views übergibst, wann sich eine eigene Datei lohnt und wie du einfache Container-Views mit eigenem Inhalt baust.

Das Problem: Die View, die immer länger wird

Jede App beginnt mit einer übersichtlichen ContentView. Dann kommt eine Liste dazu. Ein Kopfbereich. Eine Toolbar. Ein Sheet. Ein Alert. Zwei Wochen später ist body 250 Zeilen lang, die Klammern stapeln sich zehn Ebenen tief, und niemand findet mehr die Zeile, die man ändern will.

Das passiert jedem, und es ist kein Zeichen schlechter Arbeit. Es ist ein Zeichen, dass es Zeit ist aufzuräumen. In SwiftUI ist das besonders leicht, weil Views so leichtgewichtig sind (Lektion 2.1). Es kostet fast nichts, eine View in zehn kleine aufzuteilen.

Ein Beispiel zum Aufräumen

Diese Profilansicht funktioniert, ist aber schwer zu überblicken:

Swift
struct ProfileView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 24) {
                VStack(spacing: 8) {
                    Image(systemName: "person.crop.circle.fill")
                        .font(.system(size: 80))
                        .foregroundStyle(.blue)
                    Text("Anna Berger")
                        .font(.title)
                        .bold()
                    Text("iOS-Entwicklerin aus Wien")
                        .foregroundStyle(.secondary)
                }

                HStack(spacing: 12) {
                    VStack {
                        Text("128").font(.title2).bold()
                        Text("Beiträge").font(.caption).foregroundStyle(.secondary)
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color(.secondarySystemBackground))
                    .clipShape(.rect(cornerRadius: 12))

                    VStack {
                        Text("4.200").font(.title2).bold()
                        Text("Follower").font(.caption).foregroundStyle(.secondary)
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color(.secondarySystemBackground))
                    .clipShape(.rect(cornerRadius: 12))

                    VStack {
                        Text("312").font(.title2).bold()
                        Text("folge ich").font(.caption).foregroundStyle(.secondary)
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color(.secondarySystemBackground))
                    .clipShape(.rect(cornerRadius: 12))
                }
            }
            .padding()
        }
    }
}

Zwei Probleme springen ins Auge: Der Code für die drei Kacheln ist dreimal fast gleich, und man muss genau lesen, um zu erkennen, welche Klammer wohin gehört.

Werkzeug 1: Eine eigene View

Wiederholter Code wird zu einer eigenen View mit Eigenschaften. Das kennst du aus Projekt B:

Swift
struct StatTile: View {
    let value: String
    let label: String

    var body: some View {
        VStack {
            Text(value)
                .font(.title2)
                .bold()
            Text(label)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background(Color(.secondarySystemBackground))
        .clipShape(.rect(cornerRadius: 12))
    }
}

Und der Aufruf:

Swift
HStack(spacing: 12) {
    StatTile(value: "128", label: "Beiträge")
    StatTile(value: "4.200", label: "Follower")
    StatTile(value: "312", label: "folge ich")
}

Aus 30 Zeilen wurden fünf. Die Kacheln sehen garantiert gleich aus, und eine Änderung am Design passiert an einer Stelle.

Tipp

Eine eigene View bekommt einen Namen, der beschreibt, was sie ist, nicht wie sie aussieht: StatTile statt GrayBox, ProfileHeader statt TopVStack. Dann liest sich der Code der Eltern-View wie ein Inhaltsverzeichnis.

Werkzeug 2: Berechnete Eigenschaften

Nicht jeder Abschnitt wird wiederverwendet. Manchmal willst du einen Teil nur benennen, um body lesbarer zu machen. Dafür reicht eine berechnete Eigenschaft in derselben View:

Swift
struct ProfileView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 24) {
                header
                stats
            }
            .padding()
        }
    }

    private var header: some View {
        VStack(spacing: 8) {
            Image(systemName: "person.crop.circle.fill")
                .font(.system(size: 80))
                .foregroundStyle(.blue)
            Text("Anna Berger")
                .font(.title)
                .bold()
            Text("iOS-Entwicklerin aus Wien")
                .foregroundStyle(.secondary)
        }
    }

    private var stats: some View {
        HStack(spacing: 12) {
            StatTile(value: "128", label: "Beiträge")
            StatTile(value: "4.200", label: "Follower")
            StatTile(value: "312", label: "folge ich")
        }
    }
}

Jetzt liest sich body wie ein Satz: „Ein scrollbarer Bereich mit Kopf und Statistik.“ Wer Details sucht, springt zur passenden Eigenschaft.

  • private zeigt: Diese Teile sind nur für diese View gedacht.
  • some View als Typ, genau wie bei body (Lektion 1.18).

Eigene View oder Eigenschaft?

  • Berechnete Eigenschaft: Der Teil gehört nur zu dieser View, wird nicht wiederverwendet und braucht direkten Zugriff auf ihren Zustand.
  • Eigene View: Der Teil wird mehrfach verwendet, ist in sich abgeschlossen oder soll eine eigene Preview bekommen.

Im Zweifel: eigene View. Sie ist leichter zu testen und zu verschieben.

Werkzeug 3: @ViewBuilder-Funktionen

Eine berechnete Eigenschaft hat keine Parameter. Brauchst du welche, nimm eine Funktion. Enthält sie Bedingungen mit unterschiedlichen Views, braucht sie @ViewBuilder:

Swift
@ViewBuilder
private func badge(for followers: Int) -> some View {
    if followers > 1000 {
        Label("Beliebt", systemImage: "flame.fill")
            .foregroundStyle(.orange)
    } else {
        Text("Neu dabei")
            .foregroundStyle(.secondary)
    }
}

Erinnerst du dich an Lektion 1.18? Eine Funktion mit some View muss immer denselben Typ zurückgeben. Hier gibt es aber ein Label und einen Text. @ViewBuilder macht die Funktion zu einer Beschreibung wie body: Mehrere Zweige, mehrere Views untereinander, alles erlaubt.

Übrigens: body ist automatisch ein @ViewBuilder. Deshalb kannst du dort ohne Weiteres if verwenden. Berechnete Eigenschaften wie header brauchen das Attribut ebenfalls, sobald sie Bedingungen mit unterschiedlichen Views enthalten.

Daten und Aktionen übergeben

Eine Unter-View braucht oft Daten und muss manchmal etwas auslösen. Hier kommen alle Werkzeuge aus Stufe 2 zusammen:

Die Unter-View soll …Übergabe als
Daten nur anzeigenlet
Daten ändern@Binding
eine Aktion auslösen, deren Folgen die Eltern-View bestimmtClosure, z. B. let onDelete: () -> Void

Ein Beispiel mit allen dreien:

Swift
struct TaskRow: View {
    let title: String
    @Binding var isDone: Bool
    let onDelete: () -> Void

    var body: some View {
        HStack {
            Button {
                isDone.toggle()
            } label: {
                Image(systemName: isDone ? "checkmark.circle.fill" : "circle")
                    .foregroundStyle(isDone ? Color.green : Color.secondary)
            }
            .buttonStyle(.plain)

            Text(title)
                .strikethrough(isDone)

            Spacer()

            Button("Löschen", systemImage: "trash", action: onDelete)
                .labelStyle(.iconOnly)
                .buttonStyle(.borderless)
                .foregroundStyle(.red)
        }
    }
}

Und die Verwendung:

Swift
ForEach($tasks) { $task in
    TaskRow(title: task.title, isDone: $task.isDone) {
        tasks.removeAll { $0.id == task.id }
    }
}

Die Zeile weiß nicht, wo die Aufgaben gespeichert sind oder wie gelöscht wird. Sie meldet nur: „Hier wurde Löschen gedrückt.“ Das macht sie wiederverwendbar: Dieselbe TaskRow funktioniert in jeder Liste.

  • Button("Löschen", systemImage: "trash", action: onDelete) übergibt die Closure direkt als Aktion.
  • .labelStyle(.iconOnly) zeigt nur das Symbol. Der Titel bleibt für VoiceOver erhalten.

Werkzeug 4: Container mit eigenem Inhalt

Manchmal willst du eine View bauen, die beliebigen Inhalt umrahmt, so wie VStack oder Section. Zum Beispiel eine Karte mit Titel, in die man alles Mögliche hineinlegen kann:

Swift
struct Card<Content: View>: View {
    let title: String
    @ViewBuilder let content: Content

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(title)
                .font(.headline)
            content
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color(.secondarySystemBackground))
        .clipShape(.rect(cornerRadius: 16))
    }
}

Verwendet wird sie wie ein Stack, mit einer Trailing Closure:

Swift
Card(title: "Über mich") {
    Text("iOS-Entwicklerin aus Wien.")
    Text("Liebt Kaffee und Bergwandern.")
        .foregroundStyle(.secondary)
}

Das sieht kompliziert aus, aber du kennst jedes Teil:

  • Card<Content: View> ist ein Generic (Lektion 1.18): Content ist ein Platzhalter für irgendeinen View-Typ.
  • @ViewBuilder let content: Content sagt: Der Inhalt wird wie body beschrieben, mit mehreren Views und Bedingungen.
  • In body steht content einfach dort, wo der Inhalt erscheinen soll.

Genau so sind VStack, Section und NavigationStack gebaut. Du hast gerade deinen ersten eigenen Container geschrieben.

Eigene Dateien

Wird eine View in mehreren Bildschirmen verwendet oder ist sie größer als ein paar Zeilen, gehört sie in eine eigene Datei:

  1. File → New → File from Template… (⌘ N)
  2. SwiftUI View wählen
  3. Name eingeben, z. B. StatTile

Xcode legt eine Datei mit View und Preview an. Bewährt hat sich eine Ordnerstruktur nach Themen, etwa Views/Profile/, Views/Components/, Models/. Ordner legst du im Navigator per Rechtsklick → New Group an.

Tipp

Xcode kann beim Aufräumen helfen: Markiere einen Teil von body, mach einen Rechtsklick und wähle unter Refactor eine der Extract-Optionen. Je nach Xcode-Version erzeugt Xcode daraus eine Methode, Variable oder Subview, die du dann nur noch benennen und eventuell anpassen musst.

Wann ist eine View zu groß?

Es gibt keine feste Grenze, aber gute Warnsignale:

  • body passt nicht mehr auf den Bildschirm.
  • Du musst Kommentare schreiben, um Abschnitte zu erklären, wie // Kopfbereich. Der Kommentar ist ein perfekter Name für eine eigene View.
  • Derselbe Code steht zwei- oder dreimal da.
  • Die Klammern gehen mehr als fünf Ebenen tief.
  • Die Preview braucht ewig, weil eine Kleinigkeit den ganzen Bildschirm neu bauen muss.

Typische Fehler

Unterschiedliche Views ohne @ViewBuilder

Fehlermeldung

Function declares an opaque return type 'some View', but the return statements in its body do not have matching underlying types

Eine Funktion oder Eigenschaft mit Bedingungen liefert verschiedene View-Typen. Ergänze @ViewBuilder davor.

Zustand in der falschen View

Du hast einen Teil ausgelagert, und plötzlich funktioniert ein Schalter nicht mehr. Wahrscheinlich hat die neue View ein eigenes @State bekommen, wo ein @Binding hingehört (Lektion 2.10). Frag dich wieder: Wem gehört der Wert?

private blockiert den Initializer

Swift
struct StatTile: View {
    private let value: String
}

Dann kann niemand außerhalb StatTile(value:) aufrufen. Eigenschaften, die von außen übergeben werden, sind nicht private.

Zu viel aufgeteilt

Auch das gibt es: Eine View, die nur einen einzigen Text mit einem Modifier enthält, macht den Code nicht übersichtlicher, nur länger. Teile dort, wo ein sinnvoller, benennbarer Baustein entsteht.

FAQ

Anfänger

Was bedeutet das überhaupt?

Macht das Aufteilen die App langsamer? Nein. Views sind winzige Structs, und SwiftUI kommt mit vielen kleinen Views sogar besser zurecht als mit einer großen. Es kann Änderungen dann gezielter auf den betroffenen Teil beschränken (Lektion 4.5).

Wie viele Views darf eine Datei enthalten? Kleine Hilfsviews, die nur von einer View benutzt werden, dürfen gern in derselben Datei stehen, meist darunter und als private struct. Wiederverwendete Views bekommen eine eigene Datei.

Muss jede eigene View eine Preview haben? Es ist sehr empfehlenswert. Gerade kleine Komponenten wie StatTile lassen sich mit traits: .sizeThatFitsLayout und mehreren Varianten wunderbar isoliert prüfen (Lektion 2.3).

Fortgeschritten

Warum verhält es sich so?

Was ist besser: viele Parameter oder ein Model übergeben? Bekommt eine View fünf einzelne Werte, die alle aus derselben Struct stammen, übergib lieber die Struct: TaskRow(task: task). Braucht sie nur ein, zwei Werte, sind einzelne Parameter oft klarer und machen die View allgemeiner einsetzbar.

Wann schreibe ich einen eigenen Modifier statt einer View? Wenn du nicht neuen Inhalt baust, sondern bestehende Views einheitlich gestaltest, etwa „jede Karte bekommt Hintergrund, Ecken und Schatten“. Dann ist ein ViewModifier die elegantere Lösung (Lektion 3.28).

Profi

Was passiert unter der Haube?

Unterscheidet SwiftUI zwischen einer berechneten Eigenschaft und einer eigenen View? Ja, und zwar wesentlich. Eine berechnete Eigenschaft ist nur ein Teil des body der Eltern-View. Sie wird immer mit ihr zusammen neu berechnet. Eine eigene View ist ein eigener Knoten im View-Baum: SwiftUI kann ihren body überspringen, wenn sich ihre Eingaben nicht geändert haben. Bei großen Bildschirmen kann das spürbar Leistung sparen.

Warum @ViewBuilder let content: Content statt einer Closure () -> Content? Beides geht. Mit einer gespeicherten Closure würdest du sie in body aufrufen: content(). Mit dem Attribut an der Eigenschaft wertet Swift die Closure schon im Initializer aus und speichert das Ergebnis. Das ist kürzer und wird von Apple für neue Container empfohlen.

Übungen

Anfänger

Übung 1: Profil aufräumen

Nimm die aufgeräumte ProfileView aus dieser Lektion und lagere header als eigene View ProfileHeader aus, die Name und Beschreibung als Parameter bekommt. Schreib eine Preview für ProfileHeader mit einem sehr langen Namen.

Fortgeschritten

Übung 2: Einstellungszeile

Baue eine wiederverwendbare SettingsRow mit einem farbigen Symbol in einem abgerundeten Quadrat (wie in der Einstellungen-App), einem Titel und einem @Binding für einen Toggle. Verwende sie dreimal in einem Form.

Lösung Übung 2Erst selbst probieren!
Swift
struct SettingsRow: View {
    let icon: String
    let color: Color
    let title: String
    @Binding var isOn: Bool

    var body: some View {
        Toggle(isOn: $isOn) {
            HStack(spacing: 12) {
                Image(systemName: icon)
                    .foregroundStyle(.white)
                    .frame(width: 30, height: 30)
                    .background(color)
                    .clipShape(.rect(cornerRadius: 7))
                Text(title)
            }
        }
    }
}

struct SettingsView: View {
    @State private var wifi = true
    @State private var bluetooth = false
    @State private var airplane = false

    var body: some View {
        Form {
            SettingsRow(icon: "airplane", color: .orange, title: "Flugmodus", isOn: $airplane)
            SettingsRow(icon: "wifi", color: .blue, title: "WLAN", isOn: $wifi)
            SettingsRow(icon: "dot.radiowaves.left.and.right", color: .blue, title: "Bluetooth", isOn: $bluetooth)
        }
    }
}
Profi

Übung 3: Eigener Container

Baue einen Container InfoBox<Content: View> mit einem Symbol, einer Farbe und beliebigem Inhalt. Die Box hat links einen farbigen Streifen, oben das Symbol neben einer fetten Überschrift und darunter den Inhalt. Verwende sie für einen Tipp (gelb, lightbulb) und eine Warnung (rot, exclamationmark.triangle), jeweils mit mehreren Texten als Inhalt.

Zusammenfassung

  • Große Views in benennbare Bausteine zerlegen: Das macht Code lesbar, wiederverwendbar und testbar.
  • Eigene View für wiederverwendete oder abgeschlossene Teile, berechnete Eigenschaft für Abschnitte einer einzelnen View.
  • @ViewBuilder für Funktionen und Eigenschaften mit Bedingungen und unterschiedlichen Views.
  • Übergabe: let zum Anzeigen, @Binding zum Ändern, Closure zum Auslösen von Aktionen.
  • Generische Container mit @ViewBuilder let content: Content funktionieren wie VStack.
  • Wiederverwendete Views in eigene Dateien, nach Themen geordnet.

Spickzettel

Swift
// Eigene View
struct StatTile: View {
    let value: String
    let label: String
    var body: some View { ... }
}

// Abschnitt als Eigenschaft
private var header: some View { ... }

// Funktion mit Bedingung
@ViewBuilder
private func badge(for n: Int) -> some View {
    if n > 1000 { Label(...) } else { Text(...) }
}

// Aktion nach oben melden
let onDelete: () -> Void

// Eigener Container
struct Card<Content: View>: View {
    let title: String
    @ViewBuilder let content: Content
    var body: some View { VStack { Text(title); content } }
}

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.