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:
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:
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:
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:
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.
privatezeigt: Diese Teile sind nur für diese View gedacht.some Viewals Typ, genau wie beibody(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:
@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 anzeigen | let |
| Daten ändern | @Binding |
| eine Aktion auslösen, deren Folgen die Eltern-View bestimmt | Closure, z. B. let onDelete: () -> Void |
Ein Beispiel mit allen dreien:
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:
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:
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:
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):Contentist ein Platzhalter für irgendeinen View-Typ.@ViewBuilder let content: Contentsagt: Der Inhalt wird wiebodybeschrieben, mit mehreren Views und Bedingungen.- In
bodystehtcontenteinfach 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:
- File → New → File from Template… (
⌘ N) - SwiftUI View wählen
- 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:
bodypasst 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
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
Ü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.
Ü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!
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)
}
}
}Ü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.
@ViewBuilderfür Funktionen und Eigenschaften mit Bedingungen und unterschiedlichen Views.- Übergabe:
letzum Anzeigen,@Bindingzum Ändern, Closure zum Auslösen von Aktionen. - Generische Container mit
@ViewBuilder let content: Contentfunktionieren wieVStack. - Wiederverwendete Views in eigene Dateien, nach Themen geordnet.
Spickzettel
// 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 } }
}