Lernziel
Nach dieser Lektion kannst du Views mit VStack, HStack und ZStack anordnen, Abstände und Ausrichtung in Stacks steuern, mit Spacer und Divider Platz verteilen und Stacks ineinander verschachteln, um beliebige Layouts zu bauen.
Das Problem: Mehr als eine View
Bisher hast du meistens eine einzelne View gestaltet. Echte Bildschirme bestehen aber aus vielen Elementen: Ein Profilbild neben einem Namen, darunter untereinander mehrere Infos, und auf dem Bild übereinander ein kleines Abzeichen.
In SwiftUI gibt es dafür drei Grundbausteine, die Stacks (Stapel):
| Stack | Richtung | Bild |
|---|---|---|
VStack | vertikal, untereinander | Bücher auf einem Stapel |
HStack | horizontal, nebeneinander | Bücher in einem Regal |
ZStack | übereinander, in die Tiefe | Blätter auf einem Tisch |
Mit diesen dreien und etwas Verschachtelung lässt sich erstaunlich viel bauen.
VStack: Untereinander
VStack {
Text("Erste Zeile")
Text("Zweite Zeile")
Text("Dritte Zeile")
}Die Views werden von oben nach unten angeordnet, in der Reihenfolge, in der sie im Code stehen. Den VStack kennst du seit Lektion 0.4.
Abstand und Ausrichtung
Ein Stack hat zwei wichtige Parameter:
VStack(alignment: .leading, spacing: 12) {
Text("Anna Berger")
.font(.headline)
Text("iOS-Entwicklerin")
.foregroundStyle(.secondary)
Text("Wien")
.font(.footnote)
}spacing: der Abstand zwischen den Elementen in Punkten. Ohne Angabe wählt SwiftUI einen passenden Standardabstand.alignment: wie die Elemente quer zur Stapelrichtung ausgerichtet werden. BeimVStackalso horizontal:.leading(links),.center(Standard) oder.trailing(rechts).
Erwartetes Ergebnis
Drei Zeilen untereinander, alle links ausgerichtet, mit je 12 Punkten Abstand.
Achtung
alignment richtet die Elemente zueinander aus, nicht den ganzen Stack auf dem Bildschirm. Der VStack selbst ist nur so breit wie sein breitestes Element und steht weiterhin in der Mitte. Wie du ihn an den Rand schiebst, lernst du in der nächsten Lektion.
HStack: Nebeneinander
HStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.font(.largeTitle)
Text("Anna Berger")
}Hier funktioniert alignment vertikal: .top, .center (Standard), .bottom und, besonders praktisch, .firstTextBaseline:
HStack(alignment: .firstTextBaseline) {
Text("42")
.font(.largeTitle)
Text("Punkte")
.font(.caption)
}Mit .firstTextBaseline stehen beide Texte auf derselben Grundlinie, wie Wörter in einem Satz, obwohl sie unterschiedlich groß sind. Mit .center wäre „Punkte“ in der Mitte der großen Zahl, was meist unruhig aussieht.
ZStack: Übereinander
ZStack {
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
Text("AB")
.font(.title)
.foregroundStyle(.white)
}Im ZStack liegt das zuerst geschriebene Element ganz hinten, jedes weitere davor. Hier liegt also der Text auf dem Kreis.
Auch der ZStack hat ein alignment, und zwar in beide Richtungen: .topLeading, .top, .topTrailing, .leading, .center, .trailing, .bottomLeading, .bottom, .bottomTrailing.
ZStack(alignment: .bottomTrailing) {
Image(systemName: "envelope.fill")
.font(.system(size: 60))
Text("3")
.font(.caption)
.bold()
.foregroundStyle(.white)
.padding(6)
.background(.red)
.clipShape(.circle)
}Erwartetes Ergebnis
Ein Briefumschlag-Symbol mit einem roten Kreis mit der Zahl 3 in der rechten unteren Ecke.
ZStack oder background / overlay?
Oft ist .background { ... } oder .overlay { ... } die bessere Wahl als ein ZStack. Der Unterschied: Beim ZStack bestimmt das größte Element die Größe. Bei .overlay und .background bestimmt die ursprüngliche View die Größe, und das hinzugefügte Element passt sich an. Für ein Abzeichen auf einem Symbol ist deshalb .overlay(alignment: .bottomTrailing) { ... } meist eleganter.
Spacer: Platz verteilen
Ein Spacer ist eine unsichtbare View, die so viel Platz wie möglich einnimmt. Damit schiebst du Elemente auseinander:
HStack {
Text("Links")
Spacer()
Text("Rechts")
}
.padding()Der Spacer nimmt den ganzen freien Platz zwischen den beiden Texten ein, und sie landen am linken und rechten Rand.
Mit Spacern kannst du Elemente gezielt positionieren:
HStack {
Spacer()
Text("Ganz rechts")
}
VStack {
Text("Oben")
Spacer()
Text("Unten")
}Mehrere Spacer teilen sich den Platz gleichmäßig:
HStack {
Spacer()
Text("A")
Spacer()
Text("B")
Spacer()
}Spacer(minLength: 20) garantiert dabei einen Mindestabstand.
Divider: Trennlinien
VStack {
Text("Abschnitt 1")
Divider()
Text("Abschnitt 2")
}Ein Divider zeichnet eine feine Linie, im VStack waagrecht und im HStack senkrecht.
Stacks verschachteln
Die eigentliche Stärke entsteht, wenn du Stacks ineinander steckst. Eine typische Kontaktzeile:
HStack(spacing: 12) {
Image(systemName: "person.circle.fill")
.font(.system(size: 44))
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 2) {
Text("Anna Berger")
.font(.headline)
Text("Zuletzt online vor 5 Min.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "phone.fill")
.foregroundStyle(.green)
}
.padding()Erwartetes Ergebnis
Links ein blaues Personen-Symbol, daneben Name und Status untereinander, ganz rechts ein grünes Telefon-Symbol.
Lies es von außen nach innen: Eine waagrechte Reihe aus Bild, einem senkrechten Block mit zwei Texten, Leerraum und einem Symbol.
Layouts planen
Bevor du ein Layout baust, zeichne es grob auf und zieh Kästen darum: Was steht nebeneinander? Was untereinander? Was übereinander? Jeder Kasten wird ein Stack. Nach ein paar Layouts machst du das automatisch im Kopf.
Wie Stacks ihren Platz verteilen
Dieser Teil ist am Anfang etwas abstrakt, erklärt aber viele Überraschungen. SwiftUI verteilt Platz in drei Schritten:
- Die Eltern-View schlägt ihrem Kind eine Größe vor: „Du hast 390 × 800 Punkte.“
- Das Kind entscheidet selbst, wie groß es sein will: Ein Text braucht nur so viel Platz wie seine Buchstaben. Ein Spacer und eine Farbe nehmen alles, was sie bekommen.
- Die Eltern-View platziert das Kind entsprechend ihrer Ausrichtung.
Ein Stack verteilt den Platz unter seinen Kindern. Wer flexibel ist, wie Spacer oder Color, bekommt den Rest, nachdem die anderen versorgt sind.
Das erklärt zum Beispiel:
VStack {
Color.blue
Text("Hallo")
}Die blaue Fläche füllt fast den ganzen Bildschirm, denn eine Farbe nimmt so viel Platz, wie sie bekommt. Der Text nimmt nur, was er braucht.
Typische Fehler
Ausrichtung ohne Wirkung
VStack(alignment: .leading) {
Text("Kurz")
}Der Text steht trotzdem in der Mitte des Bildschirms. alignment richtet nur innerhalb des Stacks aus, und der Stack ist hier nur so breit wie der Text. Lösung in der nächsten Lektion: .frame(maxWidth: .infinity, alignment: .leading).
Spacer im falschen Stack
VStack {
Text("A")
Spacer()
Text("B")
}Wolltest du A und B nebeneinander mit Abstand? Dann gehört der Spacer in einen HStack. Ein Spacer drückt immer in Richtung seines Stacks.
Zu viele Elemente
Ein Stack kann theoretisch beliebig viele Views enthalten. Bei sehr vielen Elementen, etwa 1.000 Zeilen, solltest du aber eine List (Lektion 2.15) oder einen LazyVStack (Lektion 3.18) verwenden, die nur den sichtbaren Teil erzeugen.
Unerwartete Reihenfolge im ZStack
Ein Element verschwindet? Im ZStack liegt es vielleicht hinter einem größeren Element. Denk daran: Was zuerst geschrieben wird, liegt hinten.
FAQ
Anfänger
Was bedeutet das überhaupt?Wie tief darf ich Stacks verschachteln? So tief, wie es nötig ist. Wird der Code aber unübersichtlich, ist das ein Zeichen, Teile in eigene Views auszulagern (Lektion 2.21).
Was ist der Standardabstand, wenn ich kein spacing angebe?
SwiftUI wählt einen Abstand, der zu den enthaltenen Views passt, typischerweise etwa 8 Punkte. Wenn dir das Ergebnis wichtig ist, gib den Abstand ausdrücklich an. Mit spacing: 0 liegen die Elemente direkt aneinander.
Was ist ein „Punkt“? SwiftUI misst nicht in Pixeln, sondern in Punkten. Ein Punkt entspricht je nach Bildschirm zwei oder drei Pixeln. So sieht dein Layout auf allen iPhones gleich groß aus, egal wie scharf das Display ist.
Fortgeschritten
Warum verhält es sich so?Warum schrumpft mein Text, statt umzubrechen?
Wenn ein HStack zu wenig Platz hat, muss er entscheiden, welches Element nachgibt. Mit .layoutPriority(1) gibst du einer View Vorrang. Sie bekommt dann zuerst den Platz, den sie braucht.
Was ist Grid?
Ein Layout-Container für tabellenartige Anordnungen, bei denen Spalten über mehrere Zeilen hinweg ausgerichtet sein sollen. Für einfache Fälle reichen Stacks. Grid und LazyVGrid lernst du in Lektion 3.17.
Profi
Was passiert unter der Haube?Wie entscheidet ein Stack genau, wer wie viel Platz bekommt?
Der Stack fragt jedes Kind, wie flexibel es ist, sortiert die Kinder nach ihrer Flexibilität und verteilt den verfügbaren Platz zuerst an die unflexibelsten. Was übrig bleibt, teilen sich die flexiblen Kinder. Die layoutPriority hat dabei Vorrang vor der Flexibilität.
Wie würde ich einen eigenen Stack bauen?
Mit dem Layout-Protokoll kannst du eigene Container mit beliebiger Platzierungslogik schreiben, etwa einen Stack, der Elemente im Kreis anordnet. Das ist Thema von Lektion 4.6.
Übungen
Übung 1: Wetter-Zeile
Baue eine Zeile mit einem Wochentag links, einem Wettersymbol in der Mitte und der Temperatur rechts. Tipp: zwei Spacer.
Lösung Übung 1Erst selbst probieren!
HStack {
Text("Montag")
Spacer()
Image(systemName: "cloud.sun.fill")
.symbolRenderingMode(.multicolor)
Spacer()
Text("18 °C")
}
.padding()Achtung: Das Symbol steht nur dann genau in der Mitte, wenn die Texte links und rechts gleich breit sind. Für exakte Spalten eignet sich später Grid besser.
Übung 2: Statistik-Kacheln
Baue drei Kacheln nebeneinander. Jede zeigt groß eine Zahl und darunter klein eine Beschreibung, etwa „128 Beiträge“, „4.200 Follower“ und „312 folge ich“. Jede Kachel hat einen hellgrauen Hintergrund mit abgerundeten Ecken.
Lösung Übung 2Erst selbst probieren!
HStack(spacing: 12) {
VStack {
Text("128").font(.title2).bold()
Text("Beiträge").font(.caption).foregroundStyle(.secondary)
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(.rect(cornerRadius: 12))
VStack {
Text("4.200").font(.title2).bold()
Text("Follower").font(.caption).foregroundStyle(.secondary)
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(.rect(cornerRadius: 12))
VStack {
Text("312").font(.title2).bold()
Text("folge ich").font(.caption).foregroundStyle(.secondary)
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(.rect(cornerRadius: 12))
}Fällt dir die Wiederholung auf? In Lektion 2.21 lernst du, daraus eine eigene StatTile-View zu machen. Gleich breit werden die Kacheln mit .frame(maxWidth: .infinity) aus der nächsten Lektion.
Übung 3: App-Symbol mit Badge
Baue ein App-Symbol: ein abgerundetes Quadrat (60 × 60) mit Farbverlauf und einem weißen SF Symbol darin. Oben rechts soll ein rotes Abzeichen mit einer Zahl sitzen, das leicht über den Rand hinausragt. Löse es einmal mit ZStack und einmal mit .overlay. Welche Variante gefällt dir besser?
Lösung Übung 3Erst selbst probieren!
RoundedRectangle(cornerRadius: 14)
.fill(.blue.gradient)
.frame(width: 60, height: 60)
.overlay {
Image(systemName: "envelope.fill")
.font(.title)
.foregroundStyle(.white)
}
.overlay(alignment: .topTrailing) {
Text("5")
.font(.caption)
.bold()
.foregroundStyle(.white)
.padding(6)
.background(.red)
.clipShape(.circle)
.offset(x: 8, y: -8)
}.offset(x:y:) verschiebt das Abzeichen ein Stück nach außen. Mit .overlay bleibt das Symbol genau 60 × 60 groß. Beim ZStack würde das hinausragende Abzeichen die Gesamtgröße beeinflussen.
Zusammenfassung
VStackordnet untereinander,HStacknebeneinander,ZStackübereinander an.spacingsteuert den Abstand zwischen den Elementen,alignmentdie Ausrichtung quer zur Stapelrichtung.- Im
ZStackliegt das zuerst geschriebene Element hinten. Spacernimmt allen freien Platz und schiebt Elemente auseinander.Dividerzeichnet Trennlinien.- Komplexe Layouts entstehen durch Verschachteln.
- Eltern schlagen eine Größe vor, Kinder entscheiden selbst, Eltern platzieren.
Spickzettel
VStack(alignment: .leading, spacing: 8) { ... }
HStack(alignment: .firstTextBaseline, spacing: 12) { ... }
ZStack(alignment: .bottomTrailing) { ... }
Spacer() Spacer(minLength: 20) Divider()
HStack {
Image(...)
VStack(alignment: .leading) { Text(...); Text(...) }
Spacer()
Image(...)
}
.overlay(alignment: .topTrailing) { Badge() }