Lektion 3.26 · Stufe 3 – Fortgeschritten

Phase- und Keyframe-Animationen

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du mehrstufige Animationen choreografieren: mit phaseAnimator durch eine Folge von Zuständen laufen und mit keyframeAnimator mehrere Eigenschaften unabhängig voneinander über eine Zeitachse steuern. Du weißt, wann sich welches Werkzeug lohnt.

Das Problem: Animationen mit mehreren Schritten

Bisher hatten Animationen genau zwei Zustände: vorher und nachher. Ein Herz ist klein, dann groß. Eine Karte ist zu, dann auf.

Manche Bewegungen bestehen aber aus mehreren Schritten:

  • Ein Herz soll beim Liken erst wachsen, dann leicht zusammenschrumpfen und schließlich auf normale Größe zurückfedern.
  • Eine Glocke soll hin und her wackeln, wenn eine Nachricht kommt.
  • Ein Ladesymbol soll in einer Endlosschleife durch verschiedene Formen pulsieren.

Du könntest das mit mehreren withAnimation und Verzögerungen hintereinander basteln. Das wird schnell unübersichtlich und bricht, sobald der Nutzer zwischendurch tippt. Dafür gibt es zwei spezielle Werkzeuge.

phaseAnimator: Eine Folge von Zuständen

Ein Phase-Animator läuft durch eine Liste von Phasen, einer nach der anderen. Für jede Phase beschreibst du, wie die View aussieht.

Endlos durchlaufen

Swift
struct PulsingIcon: View {
    var body: some View {
        Image(systemName: "heart.fill")
            .font(.system(size: 60))
            .foregroundStyle(.red)
            .phaseAnimator([false, true]) { content, isLarge in
                content
                    .scaleEffect(isLarge ? 1.2 : 1)
                    .opacity(isLarge ? 1 : 0.7)
            }
    }
}
  • [false, true] sind die Phasen.
  • Die Closure bekommt den Inhalt und die aktuelle Phase und beschreibt, wie es in dieser Phase aussieht.
  • Ohne Auslöser läuft der Animator endlos: false → true → false → true …

Erwartetes Ergebnis

Das Herz pulsiert ruhig: Es wird größer und kräftiger, dann wieder kleiner und blasser, immer wieder.

Das ist dasselbe wie der pulsierende Punkt aus Lektion 3.23, nur ohne @State, ohne .onAppear und ohne .repeatForever.

Mehr als zwei Phasen mit einem Enum

Spannend wird es mit mehr Phasen. Dafür eignet sich ein Enum (Lektion 1.14):

Swift
enum LikePhase: CaseIterable {
    case start, grow, shrink, settle

    var scale: Double {
        switch self {
        case .start: 1
        case .grow: 1.5
        case .shrink: 0.9
        case .settle: 1
        }
    }

    var rotation: Double {
        switch self {
        case .grow: -12
        case .shrink: 8
        default: 0
        }
    }
}

CaseIterable liefert alle Fälle in Reihenfolge als LikePhase.allCases.

Mit Auslöser

Statt endlos soll die Animation einmal ablaufen, wenn etwas passiert, etwa bei jedem Like:

Swift
struct LikeButton: View {
    @State private var likes = 0

    var body: some View {
        Button {
            likes += 1
        } label: {
            Image(systemName: "heart.fill")
                .font(.system(size: 60))
                .foregroundStyle(.red)
        }
        .phaseAnimator(LikePhase.allCases, trigger: likes) { content, phase in
            content
                .scaleEffect(phase.scale)
                .rotationEffect(.degrees(phase.rotation))
        } animation: { phase in
            switch phase {
            case .start: .smooth(duration: 0.1)
            case .grow: .spring(duration: 0.2, bounce: 0.5)
            case .shrink: .easeOut(duration: 0.15)
            case .settle: .spring(duration: 0.3, bounce: 0.6)
            }
        }
    }
}
  • trigger: likes: Jedes Mal, wenn sich likes ändert, läuft der Animator einmal durch alle Phasen und bleibt dann wieder bei der ersten stehen.
  • animation: bestimmt für jede Phase, wie sie erreicht wird. So kann das Wachsen schnell und federnd sein und das Zurückkehren weich.

Erwartetes Ergebnis

Bei jedem Tipp wächst das Herz schwungvoll und kippt leicht nach links, schrumpft kurz und kippt nach rechts und federt dann zurück in die Ruhelage.

Die erste Phase ist die Ruhephase

Mit trigger ist die View außerhalb der Animation immer in der ersten Phase. Gestalte sie deshalb so, dass sie als Normalzustand gut aussieht, hier Skalierung 1 und keine Drehung.

keyframeAnimator: Präzise Zeitachsen

Ein Phase-Animator bewegt alle Eigenschaften gemeinsam von Phase zu Phase. Manchmal willst du aber mehr Kontrolle: Die Größe soll sich anders bewegen als die Drehung, zu unterschiedlichen Zeitpunkten. Wie in einem Videoschnittprogramm, in dem jede Eigenschaft ihre eigene Spur hat.

Genau das ist ein Keyframe-Animator. Keyframes (Schlüsselbilder) sind Punkte auf einer Zeitachse, an denen eine Eigenschaft einen bestimmten Wert haben soll.

Die animierten Werte

Zuerst fasst du alle Werte, die sich bewegen sollen, in einer Struktur zusammen:

Swift
struct BellValues {
    var rotation = 0.0
    var scale = 1.0
    var offsetY = 0.0
}

Die Startwerte sind gleichzeitig der Ruhezustand.

Spuren und Keyframes

Swift
struct RingingBell: View {
    @State private var notifications = 0

    var body: some View {
        Button {
            notifications += 1
        } label: {
            Image(systemName: "bell.fill")
                .font(.system(size: 60))
                .foregroundStyle(.orange)
        }
        .keyframeAnimator(initialValue: BellValues(), trigger: notifications) { content, value in
            content
                .rotationEffect(.degrees(value.rotation), anchor: .top)
                .scaleEffect(value.scale)
                .offset(y: value.offsetY)
        } keyframes: { _ in
            KeyframeTrack(\.rotation) {
                CubicKeyframe(20, duration: 0.1)
                CubicKeyframe(-18, duration: 0.15)
                CubicKeyframe(14, duration: 0.15)
                CubicKeyframe(-10, duration: 0.15)
                CubicKeyframe(0, duration: 0.2)
            }

            KeyframeTrack(\.scale) {
                SpringKeyframe(1.2, duration: 0.2)
                SpringKeyframe(1.0, duration: 0.5)
            }

            KeyframeTrack(\.offsetY) {
                LinearKeyframe(-10, duration: 0.15)
                SpringKeyframe(0, duration: 0.4)
            }
        }
    }
}

Was hier passiert:

  • initialValue: BellValues() gibt den Ruhezustand.
  • trigger: startet die Animation bei jeder Änderung, wie beim Phase-Animator.
  • Die erste Closure wendet die aktuellen Werte auf den Inhalt an. Sie wird für jedes Bild der Animation aufgerufen.
  • Im keyframes-Block hat jede Eigenschaft ihre eigene KeyframeTrack, ausgewählt über einen Key Path wie \.rotation.
  • In jeder Spur stehen die Keyframes hintereinander, jeweils mit Zielwert und Dauer.

Erwartetes Ergebnis

Bei jedem Tipp wackelt die Glocke von ihrer Aufhängung aus fünfmal hin und her, wird dabei kurz größer und hüpft ein Stück nach oben. Alle drei Bewegungen laufen gleichzeitig, aber jede in ihrem eigenen Takt.

anchor: .top bei der Drehung lässt die Glocke um ihren oberen Rand schwingen statt um ihre Mitte, wie eine echte Glocke.

Die Keyframe-Arten

Die Art des Keyframes bestimmt, wie der Wert dorthin gelangt:

KeyframeBewegung
LinearKeyframegleichmäßig, mit hartem Richtungswechsel
CubicKeyframeweich, mit fließenden Übergängen zwischen den Keyframes
SpringKeyframefedernd, physikalisch
MoveKeyframespringt sofort ohne Animation zum Wert

In der Praxis sind CubicKeyframe für flüssige Bewegungsabläufe und SpringKeyframe für das Ankommen am Ende am häufigsten.

Phase oder Keyframe?

Entscheidungshilfe

  • phaseAnimator: wenn sich die Animation als Folge von Zuständen beschreiben lässt: „erst groß, dann klein, dann normal“. Alle Eigenschaften wechseln gemeinsam.
  • keyframeAnimator: wenn Eigenschaften unabhängig und zeitlich genau gesteuert werden sollen: „die Drehung wackelt fünfmal, während die Größe einmal pulsiert“.
  • withAnimation: für alles mit nur zwei Zuständen. Das ist und bleibt der häufigste Fall.

Typische Fehler

Endlosanimation, obwohl sie einmal laufen sollte

Der trigger: fehlt. Ohne Auslöser läuft ein Phase-Animator endlos. Ein Keyframe-Animator mit repeating: true ebenfalls.

Animation startet nicht

Der Trigger-Wert ändert sich nicht wirklich. Ein Bool, der auf true gesetzt wird, obwohl er schon true ist, löst nichts aus. Ein Zähler, der hochgezählt wird, funktioniert immer.

Zu lang

Keyframe-Animationen verleiten zu langen Choreografien. Halte Reaktionen auf Tipps unter einer halben Sekunde. Längere Abläufe passen höchstens zu besonderen Momenten, etwa einem erreichten Ziel.

Layoutänderungen in der Closure

Swift
content.frame(width: value.width)   // ⚠️

Die Closure wird für jedes Bild aufgerufen. Effekte wie scaleEffect, offset, rotationEffect und opacity sind dafür gedacht und schnell. frame und padding ändern das Layout und können ruckeln und andere Views verschieben.

FAQ

Anfänger

Was bedeutet das überhaupt?

Was ist ein Key Path wie \.rotation? Ein Verweis auf eine Eigenschaft, ohne sie gleich auszulesen. \.rotation heißt: „die Eigenschaft rotation von BellValues“. Du kennst Key Paths schon aus sort(by: \.name) und @Query(sort: \Book.title).

Kann ich Keyframes auch ohne Tipp starten? Ja, mit repeating: true statt trigger: läuft die Animation endlos, etwa für einen Ladeindikator.

Fortgeschritten

Warum verhält es sich so?

Kann ich Keyframes außerhalb einer View nutzen? Ja, mit KeyframeTimeline. Du baust dieselben Spuren und fragst dann timeline.value(time: 0.3) ab. Das ist praktisch, um eine Animation Schritt für Schritt zu prüfen oder in einem Canvas (Lektion 4.8) zu zeichnen.

Kann ich Phase-Animatoren und normale Animationen mischen? Ja. Ein Phase-Animator kümmert sich nur um die Eigenschaften in seiner Closure. Andere Eigenschaften kannst du weiterhin mit withAnimation ändern.

Profi

Was passiert unter der Haube?

Wie unterscheiden sich phaseAnimator und keyframeAnimator intern? Der Phase-Animator setzt nacheinander neue Zustände und lässt jedes Mal eine normale Animation laufen, er ist also eine Abfolge von withAnimation. Der Keyframe-Animator berechnet dagegen aus den Spuren für jeden Zeitpunkt einen konkreten Wert und ruft damit die Inhalts-Closure auf. Deshalb kann er exakte Zeitpunkte einhalten, aber nicht wie eine Feder auf Unterbrechungen reagieren.

Was passiert, wenn der Trigger während einer Keyframe-Animation erneut auslöst? Die Animation startet von vorn, wobei die Spuren vom aktuellen Wert aus beginnen. Bei sehr schnellem Antippen kann das unruhig wirken. Für Buttons, die oft hintereinander gedrückt werden, sind deshalb Federn oft angenehmer.

Übungen

Anfänger

Übung 1: Atmender Kreis

Baue einen „Atem“-Kreis für eine Entspannungsübung: Er wird mit phaseAnimator über vier Sekunden langsam groß und hell und dann wieder klein und dunkler, endlos. Tipp: Gib im animation:-Parameter .easeInOut(duration: 4) zurück.

Lösung Übung 1Erst selbst probieren!
Swift
struct BreathingCircle: View {
    var body: some View {
        Circle()
            .fill(.teal.gradient)
            .frame(width: 150, height: 150)
            .phaseAnimator([false, true]) { content, isInhaling in
                content
                    .scaleEffect(isInhaling ? 1.4 : 0.8)
                    .opacity(isInhaling ? 1 : 0.6)
            } animation: { _ in
                .easeInOut(duration: 4)
            }
    }
}
Fortgeschritten

Übung 2: Falsches Passwort

Baue ein Passwortfeld. Ist die Eingabe beim Tipp auf „Anmelden“ falsch, wackelt das Feld mit keyframeAnimator schnell nach links und rechts, wie in iOS bei falschem Code. Verwende eine Spur für offsetX mit fünf CubicKeyframes.

Profi

Übung 3: Erfolgs-Animation

Baue eine Erfolgsanzeige für ein erreichtes Ziel: Ein Häkchen-Symbol erscheint klein und durchsichtig, wächst über die Originalgröße hinaus, federt zurück, dreht sich dabei einmal leicht und wirft am Ende einen pulsierenden Schatten. Verwende einen keyframeAnimator mit mindestens drei Spuren. Berücksichtige accessibilityReduceMotion.

Zusammenfassung

  • phaseAnimator läuft durch eine Folge von Phasen. Ohne trigger endlos, mit trigger einmal pro Änderung.
  • Ein Enum mit CaseIterable beschreibt mehrere Phasen übersichtlich.
  • Der animation:-Parameter bestimmt pro Phase die Kurve.
  • keyframeAnimator steuert mehrere Eigenschaften auf eigenen Spuren mit genauen Zeitpunkten.
  • Keyframe-Arten: Linear, Cubic, Spring, Move.
  • In den Closures nur Effekte verwenden, keine Layoutänderungen.

Spickzettel

Swift
// Phasen
.phaseAnimator(Phase.allCases, trigger: count) { content, phase in
    content.scaleEffect(phase.scale)
} animation: { phase in
    .spring(duration: 0.2)
}

// Keyframes
struct Values { var angle = 0.0; var scale = 1.0 }

.keyframeAnimator(initialValue: Values(), trigger: count) { content, v in
    content.rotationEffect(.degrees(v.angle)).scaleEffect(v.scale)
} keyframes: { _ in
    KeyframeTrack(\.angle) {
        CubicKeyframe(15, duration: 0.1)
        CubicKeyframe(0, duration: 0.2)
    }
    KeyframeTrack(\.scale) {
        SpringKeyframe(1.2, duration: 0.15)
        SpringKeyframe(1, duration: 0.3)
    }
}

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.