Lektion 3.24 · Stufe 3 – Fortgeschritten

Transitions

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du steuern, wie Views erscheinen und verschwinden: mit eingebauten Transitions wie .opacity, .scale, .slide und .move, mit kombinierten und asymmetrischen Transitions und mit eigenen Transitions. Du verstehst, warum Transitions eine Animation brauchen und warum sich manchmal ganz unerwartet eine View austauscht.

Das Problem: Einfaches Einblenden reicht nicht immer

In Lektion 3.23 hast du gesehen: Ein if mit withAnimation blendet eine View sanft ein und aus. Das ist ein guter Standard. Aber oft passt einfaches Einblenden nicht:

  • Eine Benachrichtigung soll von oben hereinrutschen und nach oben wieder verschwinden.
  • Ein Hinweis soll aus dem Nichts aufploppen.
  • Eine Karte soll von rechts kommen und nach links gehen, wie beim Durchblättern.

Diese Art, wie eine View den Bildschirm betritt und verlässt, heißt Transition.

Die Grundidee

Transitions wirken nur bei Views, die eingefügt oder entfernt werden, typischerweise durch ein if:

Swift
struct BannerDemo: View {
    @State private var showBanner = false

    var body: some View {
        VStack {
            if showBanner {
                Text("Gespeichert!")
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(.green)
                    .foregroundStyle(.white)
                    .transition(.move(edge: .top))
            }

            Spacer()

            Button("Banner umschalten") {
                withAnimation {
                    showBanner.toggle()
                }
            }
        }
    }
}

Erwartetes Ergebnis

Ein Tipp lässt den grünen Banner von oben hereinrutschen. Ein weiterer Tipp schiebt ihn wieder nach oben hinaus.

Zwei Dinge sind nötig:

  1. .transition(...) an der View, die erscheint oder verschwindet, beschreibt wie.
  2. Eine Animation, hier withAnimation, sorgt dafür, dass es animiert wird.

Ohne Animation keine Transition

Fehlt withAnimation (oder .animation(_:value:) am umgebenden Container), erscheint und verschwindet die View sofort. Die Transition wird nur ausgeführt, wenn die Änderung animiert ist.

Die eingebauten Transitions

Swift
.transition(.opacity)                 // ein- und ausblenden (Standard)
.transition(.scale)                   // aus der Mitte wachsen, dorthin schrumpfen
.transition(.scale(scale: 0.5, anchor: .bottom))
.transition(.slide)                   // links rein, rechts raus
.transition(.move(edge: .bottom))     // von einer Kante herein und dorthin zurück
.transition(.push(from: .trailing))   // schiebt sich herein und blendet dabei
.transition(.blurReplace)             // mit Unschärfe ein- und ausblenden
  • .opacity ist der Standard, wenn du nichts angibst.
  • .scale wächst aus dem Mittelpunkt. Mit anchor: von einem anderen Punkt aus, etwa unten.
  • .slide kommt immer von links und geht nach rechts.
  • .move(edge:) kommt von einer Kante und geht zur selben Kante zurück.
  • .push(from:) und .blurReplace eignen sich besonders, wenn eine View eine andere ersetzt.

Transitions kombinieren

Mit .combined(with:) wirken mehrere Transitions gleichzeitig:

Swift
.transition(.scale.combined(with: .opacity))
.transition(.move(edge: .bottom).combined(with: .opacity))

Die erste Zeile lässt eine View gleichzeitig wachsen und einblenden, ein typisches „Aufploppen“. Die zweite rutscht von unten herein und wird dabei sichtbar.

Asymmetrische Transitions

Manchmal soll eine View anders gehen, als sie gekommen ist. Etwa eine Karte, die von rechts kommt und nach links verschwindet:

Swift
.transition(.asymmetric(
    insertion: .move(edge: .trailing),
    removal: .move(edge: .leading)
))
  • insertion: wie die View erscheint
  • removal: wie sie verschwindet

Views austauschen

Ein häufiger Fall: Statt eine View ein- und auszublenden, wird sie durch eine andere ersetzt. Zum Beispiel beim Durchblättern von Karten:

Swift
struct CardSwitcher: View {
    let words = ["Hallo", "Bonjour", "Hola", "Ciao", "Hej"]
    @State private var index = 0

    var body: some View {
        VStack(spacing: 30) {
            Text(words[index])
                .font(.largeTitle.bold())
                .frame(width: 240, height: 140)
                .background(.indigo.gradient)
                .foregroundStyle(.white)
                .clipShape(.rect(cornerRadius: 20))
                .id(index)
                .transition(.asymmetric(
                    insertion: .move(edge: .trailing).combined(with: .opacity),
                    removal: .move(edge: .leading).combined(with: .opacity)
                ))

            Button("Weiter") {
                withAnimation(.snappy) {
                    index = (index + 1) % words.count
                }
            }
        }
    }
}

Das Entscheidende ist .id(index). Ohne .id sieht SwiftUI immer dieselbe Text-View, deren Inhalt sich ändert, und blendet nur den Text über. Mit .id(index) bekommt die View bei jedem neuen Index eine neue Identität: SwiftUI hält sie für eine neue View. Die alte wird entfernt (removal), die neue eingefügt (insertion), und beide Transitions laufen gleichzeitig.

Erwartetes Ergebnis

Bei jedem Tipp auf „Weiter“ gleitet die alte Karte nach links hinaus, während die neue von rechts hereinkommt.

Identität

Was SwiftUI als „dieselbe“ View betrachtet, heißt Identität. Es ist eines der wichtigsten Konzepte, um SwiftUI wirklich zu verstehen. .id() ist ein Weg, sie bewusst zu verändern. Mehr dazu in Lektion 4.1.

Inhalt statt View wechseln

Willst du nicht zwei Views tauschen, sondern nur den Inhalt einer View schön wechseln, gibt es contentTransition (Lektion 3.23):

Swift
Text(status)
    .contentTransition(.interpolate)

.interpolate versucht, zwischen den Buchstabenformen zu überblenden. Für Zahlen ist .numericText() die beste Wahl.

Transitions in Listen

In einem ForEach gelten Transitions für Elemente, die hinzukommen oder entfernt werden:

Swift
struct NotificationList: View {
    @State private var messages: [String] = []

    var body: some View {
        VStack {
            ForEach(messages, id: \.self) { message in
                Text(message)
                    .padding()
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .background(.blue.opacity(0.15))
                    .clipShape(.rect(cornerRadius: 12))
                    .transition(.move(edge: .top).combined(with: .opacity))
            }

            Spacer()

            Button("Neue Nachricht") {
                withAnimation(.bouncy) {
                    messages.insert("Nachricht \(messages.count + 1)", at: 0)
                }
            }
        }
        .padding()
    }
}

Jede neue Nachricht rutscht von oben herein, und die bestehenden rücken animiert nach unten. Das funktioniert, weil jedes Element über seine id erkennbar bleibt.

Achtung

In einer List steuert das System die Animationen für Einfügen und Löschen selbst. Eigene .transitions werden dort meist ignoriert. Für eigene Effekte nimm eine ScrollView mit LazyVStack.

Eigene Transitions

Wenn keine der eingebauten passt, baust du deine eigene. Eine Transition beschreibt, wie eine View im Zustand „gerade erscheinend bzw. verschwindend“ (isIdentity == false) und im normalen Zustand (isIdentity == true) aussieht:

Swift
struct TwirlTransition: Transition {
    func body(content: Content, phase: TransitionPhase) -> some View {
        content
            .scaleEffect(phase.isIdentity ? 1 : 0.3)
            .rotationEffect(.degrees(phase.isIdentity ? 0 : 180))
            .opacity(phase.isIdentity ? 1 : 0)
    }
}

extension Transition where Self == TwirlTransition {
    static var twirl: TwirlTransition { TwirlTransition() }
}

Dann:

Swift
.transition(.twirl)

Die View dreht sich beim Erscheinen aus einer halben Umdrehung in die Normallage und wächst dabei.

Die Erweiterung mit where Self == TwirlTransition sorgt dafür, dass du .twirl schreiben kannst wie bei den eingebauten Transitions. Das ist dasselbe Muster wie bei eigenen Styles (Lektion 3.28).

Tipp

phase kann drei Werte haben: .willAppear (vor dem Erscheinen), .identity (normal sichtbar) und .didDisappear (nach dem Verschwinden). Mit phase == .willAppear kannst du also für Erscheinen und Verschwinden unterschiedliche Effekte bauen, etwa nach rechts drehen beim Kommen und nach links beim Gehen.

Typische Fehler

Transition wird nicht ausgeführt

  • Die Änderung ist nicht animiert. Füge withAnimation hinzu.
  • Die View wird gar nicht eingefügt oder entfernt, sondern nur verändert. Transitions wirken nur bei if, switch, ForEach oder geänderter .id.

Transition beim Verschwinden funktioniert nicht

Die .animation(_:value:) hängt an der View, die verschwindet. Die ist danach weg und kann nichts mehr animieren. Häng die Animation an den umgebenden Container oder verwende withAnimation.

Die View springt an ihren Platz

Transitions beschreiben nur Erscheinen und Verschwinden. Wie die anderen Views ausweichen, bestimmt die Animation. Ist die Animation sehr kurz oder .linear, wirkt es ruckelig. Eine Feder sieht meist am besten aus.

.id() zu weit außen

Setzt du .id() an einen großen Container, wird bei jeder Änderung alles darin neu erzeugt, einschließlich seines Zustands. Häng .id() so eng wie möglich an die View, die ausgetauscht werden soll.

FAQ

Anfänger

Was bedeutet das überhaupt?

Warum gibt es Transition und Animation getrennt? Die Animation bestimmt Tempo und Kurve, die Transition die Art der Bewegung beim Erscheinen. Dieselbe Transition kann schnell oder langsam, federnd oder gleichmäßig ablaufen.

Kann ich Transitions bei Sheets ändern? Nein, Sheets und Navigation haben feste System-Übergänge. Für eigene Effekte baust du ein „Overlay“ selbst mit ZStack, if und einer Transition.

Fortgeschritten

Warum verhält es sich so?

Was ist der Unterschied zwischen .move(edge:) und .offset? .offset verschiebt eine View, die bereits da ist. .move(edge:) beschreibt nur, wie eine View erscheint oder verschwindet. Für Bewegungen einer sichtbaren View nimm offset mit einer Animation, für Erscheinen und Verschwinden eine Transition.

Wie animiere ich den Wechsel zwischen zwei ganz verschiedenen Views in einem switch? Jeder Fall des switch ist für SwiftUI eine eigene View. Gib allen Fällen dieselbe .transition und ändere den Zustand mit withAnimation. Für einen nahtlosen Wechsel eignet sich .blurReplace oder .push(from:) besonders gut.

Profi

Was passiert unter der Haube?

Wie funktioniert eine Transition intern? Beim Einfügen rendert SwiftUI die View zuerst mit phase == .willAppear und animiert dann zur Identität. Beim Entfernen behält SwiftUI die View vorübergehend im Baum, animiert zu .didDisappear und entfernt sie erst danach. Deshalb ist die View während der Removal-Animation noch zu sehen, obwohl sie im Zustand schon fehlt.

Warum verwendet Transition ein body mit some View? Es ist ein Protokoll mit einer View-Funktion, ähnlich wie ViewModifier (Lektion 3.28). Jede Transition ist damit im Grunde ein Modifier, der abhängig von der Phase angewendet wird. Das macht eigene Transitions genauso flexibel wie eigene Modifier.

Übungen

Anfänger

Übung 1: Hinweis-Popup

Zeig auf Knopfdruck einen kleinen Hinweis „Kopiert!“, der mit .scale.combined(with: .opacity) erscheint. Nach zwei Sekunden verschwindet er wieder von selbst. Tipp: .task(id:) mit try? await Task.sleep(for: .seconds(2)) (Lektion 3.7).

Lösung Übung 1Erst selbst probieren!
Swift
struct CopyHint: View {
    @State private var showHint = false

    var body: some View {
        ZStack {
            Button("Kopieren") {
                withAnimation(.bouncy) { showHint = true }
            }

            if showHint {
                Text("Kopiert!")
                    .padding(.horizontal, 16)
                    .padding(.vertical, 10)
                    .background(.ultraThinMaterial)
                    .clipShape(.capsule)
                    .offset(y: -60)
                    .transition(.scale.combined(with: .opacity))
            }
        }
        .task(id: showHint) {
            guard showHint else { return }
            try? await Task.sleep(for: .seconds(2))
            withAnimation { showHint = false }
        }
    }
}

.task(id: showHint) startet jedes Mal neu, wenn sich showHint ändert. Nur wenn der Hinweis gerade sichtbar ist, wartet es zwei Sekunden und blendet ihn dann aus.

Fortgeschritten

Übung 2: Schritt-für-Schritt

Baue einen Ablauf mit drei Schritten (z. B. „Name“, „E-Mail“, „Fertig“). Mit „Weiter“ gleitet der aktuelle Schritt nach links hinaus und der nächste von rechts herein. Mit „Zurück“ soll es genau umgekehrt sein. Tipp: Merk dir die Richtung in einem State und wähle die asymmetrische Transition davon abhängig.

Profi

Übung 3: Eigene Transition mit Richtung

Schreib eine eigene Transition, die beim Erscheinen mit Unschärfe von unten hereinkommt und beim Verschwinden nach oben weggeht und dabei kleiner wird. Unterscheide dazu .willAppear und .didDisappear.

Zusammenfassung

  • Transitions beschreiben, wie Views erscheinen und verschwinden.
  • Sie brauchen immer eine Animation, etwa withAnimation.
  • Eingebaut: .opacity, .scale, .slide, .move(edge:), .push(from:), .blurReplace.
  • .combined(with:) kombiniert, .asymmetric unterscheidet Kommen und Gehen.
  • .id(wert) macht aus einer geänderten View eine neue, sodass Transitions beim Austauschen greifen.
  • Eigene Transitions mit dem Transition-Protokoll und phase.isIdentity.

Spickzettel

Swift
if isVisible {
    Banner()
        .transition(.move(edge: .top).combined(with: .opacity))
}
Button("Zeigen") { withAnimation { isVisible.toggle() } }

.transition(.asymmetric(insertion: .move(edge: .trailing),
                        removal: .move(edge: .leading)))

Card(item: items[i])
    .id(i)                       // neue Identität → Transition
    .transition(.push(from: .trailing))

struct MyTransition: Transition {
    func body(content: Content, phase: TransitionPhase) -> some View {
        content.opacity(phase.isIdentity ? 1 : 0)
    }
}

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.