LANARS

DER ULTIMATIVE LEITFADEN ZUR ERSTELLUNG EINES SEITENMENÜS IN SWIFTUI: TEIL 2. BENUTZERDEFINIERTE ÜBERGÄNGE UND ANIMATIONEN

DER ULTIMATIVE LEITFADEN ZUR ERSTELLUNG EINES SEITENMENÜS IN SWIFTUI: TEIL 2. BENUTZERDEFINIERTE ÜBERGÄNGE UND ANIMATIONEN
Zeit zum Lesen
24 min
Abschnitt
Teilen
Abonnieren
Diese Website ist durch reCAPTCHA geschützt und es gelten die Datenschutz-Bestimmungen und Nutzungsbedingungen von Google.

WILLKOMMEN ZURÜCK

Hallo zusammen! In Teil 1 dieser Artikelserie haben wir uns damit beschäftigt, wie Sie mit Label, LabelStyle, ButtonStyle und anderen Ihre eigenen Komponenten im SwiftUI-Design erstellen. Außerdem konnten Sie erleben, wie einfach es ist, einen eigenen Container zu entwerfen und Präsentationsinformationen an Geschwister zu übergeben, so wie es Apple mit seinen Sheet und ähnlichen Präsentationscontainern macht.

Heute werden wir unsere Reise in die SwiftUI-Welt fortsetzen, indem wir lernen, wie Sie Ihre eigenen Übergänge erstellen, wie Sie ausgefallene Animationen machen und wie Sie Layoutdaten in der deklarativen Welt erhalten.

 

AUSWAHL

In diesem Abschnitt implementieren wir den Umgang mit der Auswahl eines bestimmten Eintrags in der Liste. Sie können mit diesem Commit dort beginnen, wo wir im vorherigen Teil aufgehört haben.

Zur Erinnerung, was wir implementieren müssen:

Anchor und AnchorPreference

Wie Sie in der Vorschau oben sehen können, werden wir genau eine Schaltfläche hervorheben. Wir könnten den Hintergrund für eine ausgewählte Schaltfläche aktivieren und deaktivieren, aber das ist weder interessant noch anspruchsvoll und würde zu einer einfachen Ein- und Ausblendungsanimation führen.

Das obige Beispiel sieht so aus, als ob der Hintergrund animiert wird, indem er von einer Schaltfläche zu einer anderen bewegt wird. 

In einfachem UIKit könnten wir Frames für jede Unteransicht auslesen und dann eine einzelne Hintergrundansicht an eine geeignete Position verschieben, aber was ist mit SwiftUI? Die deklarative Natur von SwiftUI erlaubt es uns nicht einmal, diese Daten direkt zu lesen, aber es gibt nur wenige Tools, die dieses Problem lösen:

  • GeometryReadereine spezielle Ansicht, die Ihnen Zugriff auf einen GeometryProxy gibt - einen Proxy für verschiedene Geometrieeigenschaften wie Rahmen, Größe und einen Anker. 
  • matchedGeometryEffectein Modifikator, mit dem Sie Geometrie in einer Gruppe von Ansichten synchronisieren können. Wir empfehlen diese Artikelserie von swiftui-lab.com, um ein besseres Verständnis für das Was und Wie zu bekommen.
  • anchorPreferencesein Modifikator, mit dem Sie Geometriemerkmale aus dem Koordinatenraum einer bestimmten Ansicht in den Einstellungen speichern und dann über GeometryReader in Ihren Koordinatenraum konvertieren können

Sicherlich ist dies keine vollständige Liste der verfügbaren Modifikatoren, Ansichten und Effekte, aber für unsere Aufgabe mehr als ausreichend. 

Wir beginnen mit der ersten und der letzten Option in Verbindung. Hier sehen Sie, was für eine dynamische Auswahl, die die gesamte Schaltfläche hervorhebt, getan werden kann:

  1. Ermitteln Sie seine Grenzen in einem lokalen Koordinatenraum über die Ankereinstellungen. 
  2. Speichern Sie sie in den Einstellungen. Als Ergebnis haben wir ein Verzeichnis der Begrenzungen für jeden Menüpunkt.
  3. Wenn Sie einen Menüpunkt auswählen, ändern Sie den Hintergrund der Auswahl in die Grenzen des entsprechenden Punktes, die in unseren Koordinatenraum konvertiert wurden. 

Es ist an der Zeit, den obigen Plan mit praktischem Wissen zu verbinden. Zuallererst müssen wir den empfangenen Anker irgendwo speicherndas verlangt die Methodensignatur anchorPreference(key:value:transform:) von uns. Erstellen Sie eine neue Datei namens MenuItemGeometryPreferenceKey:

struct MenuItemGeometry {

  let item: MenuItem
  let bounds: Anchor<CGRect>
}

struct MenuItemGeometryPreferenceKey: PreferenceKey {

  static var defaultValue: [MenuItem: MenuItemGeometry] = [:]

  static func reduce(value: inout [MenuItem : MenuItemGeometry], nextValue: () -> [MenuItem: MenuItemGeometry]) {
    value.merge(nextValue()) { _, rhs in rhs }
  }
}

Nehmen Sie sich einen Moment Zeit, um zu überprüfen, was wir soeben getan haben: Diese Einstellung speichert einen Anchor pro Element. Wir werden später auf die Art und die Möglichkeiten von Anchor zurückkommen, aber jetzt können wir die Geometriedaten der einzelnen Schaltflächen erhalten. Gehen Sie dazu zur MenuItemList und aktualisieren Sie die Eigenschaft itemsList wie folgt:

@ViewBuilder
private var itemsList: some View {
  VStack(alignment: .leading) {
    ForEach(items) { item in
      Button(
        action: {
          if selection == item {
            selection = nil
          } else {
            selection = item
          }
        },
        label: {
          Label(item: item)
        }
      )
      .buttonStyle(MenuItemButtonStyle(isSelected: item == selection))
      .anchorPreference(key: MenuItemGeometryPreferenceKey.self, value: .bounds) {
        [item: $0]
      }
    }
  }
  .backgroundPreferenceValue(MenuItemGeometryPreferenceKey.self, alignment: .topLeading) { preferences in
    fatalError("To be implemented")
  }
}

Auch dieses Mal ist nichts Besonderes, aber dieser Code erfüllt unsere erste Anforderung: Indem wir für jede Schaltfläche einen anchorPreference-Modifikator hinzufügen, übergeben wir ihn über die Voreinstellungshierarchie weiter oben an den backgroundPreferenceValue, wobei preferences vom Typ [MenuItem: Anchor<CGRect>] ist. So haben wir alle Rahmen an einem Ort, aber was macht backgroundPreferenceValue selbst? Apple ist sich darüber im Klaren

Er liest den angegebenen Einstellungswert aus der Ansicht und verwendet ihn, um eine zweite Ansicht zu erzeugen, die als Hintergrund für die ursprüngliche Ansicht verwendet wird.

Das ist die perfekte Stelle, um unseren Auswahlhintergrund zurückzugeben. Fügen Sie den folgenden Code unter unserer Eigenschaft itemsList ein:

@ViewBuilder
private func selectionBackground(from preferences: MenuItemGeometryPreferenceKey.Value, in geometry: GeometryProxy) -> some View {
  if let selection, let frame = preferences[selection].map({ geometry[$0] }) {
    RoundedRectangle(cornerRadius: 13)
      .frame(width: frame.width, height: frame.height)
      .offset(x: frame.minX, y: frame.minY)
  }
}

Um diese neue Ansicht in unserer Liste zu verwenden, müssen wir lediglich einen backgroundPreferenceValue-Body aus der obigen itemsList aktualisieren:

.backgroundPreferenceValue(MenuItemGeometryPreferenceKey.self, alignment: .topLeading) { preferences in
  GeometryReader { proxy in
    selectionBackground(from: preferences, in: proxy)
      .foregroundColor(Color("color/selectionBackground"))
  }
}

Bevor wir uns in die Erklärungen vertiefen, lassen Sie uns endlich einen Build durchführen, um zu sehen, was wir haben

Wow, das sieht schon so aus, wie wir es ursprünglich wollten. Aber noch nicht schick genug, also werden wir die Auswahlanimation in ein paar Minuten noch weiter verbessern. Jetzt ist es an der Zeit, zu erklären, was wir oben gemacht haben und warum.

In den vorangegangenen Schritten der Implementierung haben wir die geometrischen Merkmale (Rahmen) jeder Schaltfläche in einer Einstellung erfasst. Danach konnten wir unseren Auswahlhintergrund auf einen Rahmen einer ausgewählten Ansicht verschieben oder sogar ganz verschwinden lassen, falls es überhaupt keine Auswahl gibt.

Wie Sie feststellen konnten, fungiert Anchor<CGRect>, das in einem Wörterbuch gesammelt wurde, als Proxy, der weiß, wie man exakte Geometriemerkmale in einen bestimmten Koordinatenraum übersetzt. Und der GeometryReader, der unseren selectionBackground enthält, definiert ihn automatisch. 

Außerdem können wir in SwiftUI den Rahmen nicht direkt festlegen. Stattdessen sind wir gezwungen, zuerst die Ansicht zu verschieben und eine Rahmengröße festzulegen.

Wir könnten hier einfach aufhören und zu einem Hintergrundübergang übergehen, aber wir sind noch nicht fertig. Fügen wir einen subtilen Übergangseffekt hinzu, wenn die Auswahl von einer Schaltfläche zu einer anderen wechselt:

Wow, das sieht ja toll aus, nicht wahr? Aber wie können wir diesen Effekt mit SwiftUI erreichen? 

Scrollen Sie nicht gleich nach unten, sondern überlegen Sie sich erst einmal, wie das möglich ist. 

Sind Sie bereit, weiterzumachen? Ok, die Idee ist denkbar einfach und in SwiftUI überraschend kurz: oben auf unserer Artikelliste können wir eine bereits ausgewählte Liste mit einem Hintergrund hinzufügen, der mit einer Auswahlfarbe gefüllt ist. Dann müssen wir nur noch die Maske verschieben, die dieses Overlay über unserer Liste "anzeigt". Schwer vorstellbar? Nehmen Sie sich Zeit, und wenn Sie fertig sind, ist es an der Zeit, der Eigenschaft MenuItemList.itemsList weiteren Code hinzuzufügen. Aber dieses Mal verwenden wir anstelle von backgroundPreferenceValue sein GegenstückoverlayPreferenceValue:

.overlayPreferenceValue(MenuItemGeometryPreferenceKey.self, alignment: .topLeading) { value in
  VStack(alignment: .leading) {
    ForEach(items) { item in
      Button(
        action: { /* noop */ },
        label: { Label(item: item) }
      )
      .buttonStyle(MenuItemButtonStyle(isSelected: true))
    }
  }
  .background(Color("color/selectionBackground"))
}

Oh. Das sieht noch nicht wie eine fertige Version aus, also fügen wir dem VStack endlich eine Maske aus dem obigen Code hinzu

.overlayPreferenceValue(MenuItemGeometryPreferenceKey.self, alignment: .topLeading) { value in
  VStack(alignment: .leading) {
  ...
  .background(Color("color/selectionBackground"))
 .mask {
    GeometryReader { proxy in
      selectionBackground(from: value, in: proxy)
    }
  }
}

Ok, jetzt haben wir das gleiche Aussehen wie vor der Maske erreicht, aber der Versuch, eine Auswahl zu treffen, führt zu nichts. Das Problem ist, dass wir ein Overlay voller Schaltflächen hinzugefügt haben, die alle auf unsere Berührungen reagieren, aber der Aktionsabschluss ist leer. Und das geschieht absichtlich. Lassen Sie mich eine letzte Zeile nach dem Maskenmodifikator hinzufügen und sie erklären:

.overlayPreferenceValue(MenuItemGeometryPreferenceKey.self, alignment: .topLeading) { value in
  VStack(alignment: .leading) {
  ...
  .background(...)
  .mask {  
    ...
  }
  .allowsHitTesting(false)
}

Nach dieser kleinen Änderung können Sie die Animation von einem ausgewählten Menüpunkt zu einem anderen erstellen und ausführen, um eine glatte Animation zu erhalten.

Aber warum haben wir die Berührungen nicht im Overlay behandelt? Damit würden wir nämlich die Berührungen aus der ursprünglichen Liste übernehmen, so dass diese keine Informationen darüber enthält, ob eine Taste gedrückt wird oder nicht. Auf den ersten Blick ist das keine große Sache, aber dadurch wird ein sehr wichtiger Teil der Benutzerfreundlichkeit beeinträchtigt - das visuelle Feedback, da keine Hervorhebungsanimation des ausgewählten Menüelements erfolgt. Sie können experimentieren, um das Ergebnis zu sehen. Deshalb haben wir einfach das gesamte Overlay von einer Benutzerinteraktion ausgeschlossen, obwohl es auf dem Bildschirm dargestellt wird.

Die endgültige Version finden Sie hier.

BENUTZERDEFINIERTER ÜBERGANG

Zusätzlich zur Auswahl möchten wir auch einen benutzerdefinierten Übergang zwischen den Farbschemata haben. Wie Sie ganz am Anfang des Artikels sehen können, gibt es einen Picker, der genauso wie der Rest des Menüs erweitert/zusammengeklappt werden kann.

Außerdem wird bei der Auswahl eines anderen Farbschemas ein neuer Hintergrund eingeblendet, der die Form des Umschalters wiederholt. Doch bevor wir beginnen, fügen wir den bereits vorhandenen ColorSchemePicker zum Menü in unser Design ein: Er sollte direkt nach allen Menüpunkten erscheinen. Diese Ansicht ist in der Beispielanwendung bereits vorhanden, so dass wir sie nur noch in der MenuView hinzufügen müssen:

var body: some View {
  VStack {
    MenuItemList(selection: $selection)
    ...
    Spacer()
      .frame(height: 10)

    ColorSchemePicker(selection: Binding($colorScheme, default: systemColorScheme))
      .overrideColorScheme($colorScheme.animation(.default))

    ...
  }
  ...
}

Sie werden den Modifikator overrideColorScheme bemerken, den wir bisher noch nicht behandelt haben und der in der SwiftUI-Bibliothek nicht enthalten ist. Sein Zweck ist es, das Farbschema des zugehörigen UIWindow zu überschreiben. Der Grund für diese Vorgehensweise ist, dass preferredColorScheme das Farbschema überschreibt. Wenn das Menü jedoch verschwindet, gibt es keine Ansicht mehr in hierarhcy, die ein bevorzugtes Farbschema festlegt, so dass es automatisch zurückgesetzt wird, sobald das Menü vom Bildschirm verschwindet. Sie können die Implementierung untersuchen, wenn Sie möchten. Wenn es im Code etwas gibt, das wir nicht abgedeckt haben, erkunden Sie bitte das Quellcode-Repository

Sobald das erledigt ist, können Sie sich ansehen, wie sich unser Menü langsam und stetig in das Menü aus dem Gif in Teil 1 verwandelt:

Aber wenn Sie jetzt eines der Farbschemata auswählen, führt die Benutzeroberfläche einfach eine Überblendung aus. Was wir hier wirklich wollen, ist unsere formabhängige Einblendung. Nehmen Sie sich einen Moment Zeit und überlegen Sie, wie Sie dies Schritt für Schritt erreichen würden, ohne SwiftUI zu berücksichtigen. Sind Sie bereit, weiterzumachen? 

Ok, so können wir das erreichen: Es sollte eine Form mit einer Farbe geben, die wir gerne hätten und die als Hintergrund dienen würde. Wir können entweder den gesamten Hintergrund ausschneiden oder ihn skalieren, aber das ist hier nicht so wichtig.

Als Nächstes brauchen wir einen Rahmen, von dem aus wir mit dem Einblenden beginnen können, und natürlich einen Rahmen, auf den wir unsere Form skalieren sollten. Wenn wir zur Benutzeroberfläche zurückkehren, wird deutlich, dass der anfängliche Rahmen derselbe ist, den auch Picker hat, während der endgültige Rahmen die Grenzen der gesamten Ansicht umfasst.

AnyTransition

Haben Sie sich jemals gefragt, wie AnyTransition funktioniert? Wir empfehlen Ihnen dringend, eine Reihe von Artikeln über Animationen aus swiftui-lab.com zu lesen: Animationen und speziell Fortgeschrittene Übergänge, damit Sie ein wirklich tiefes Verständnis für die Möglichkeiten von Übergängen und Animationen bekommen. Lassen Sie uns kurz ein Beispiel für einen Deckkraftübergang durchgehen und dann unseren Shape-Fade-Übergang implementieren.

ViewModifier und AnyTransition.modifier(active:idenity:) — das ist buchstäblich alles, was sich hinter der Magie des Übergangs verbirgt. SwiftUI identifiziert durch seinen Mechanismus die Ansicht, die erscheint oder verschwindet, und animiert den Unterschied zwischen zwei Zuständen durch die angegebenen Modifikatoren. Es ist wichtig zu verstehen, dass der Modifikator auch nach dem Erscheinen der Ansicht angewendet wird, wenn es sich um einen Einfügeübergang handelt.

Sie konnten vor einigen Augenblicken eine andere Animation sehen, aber woher weiß SwiftUI, was sich geändert hat? An dieser Stelle kommt der zweite Spieler ins Spiel — Animatable

public protocol Animatable {
    /// The type defining the data to animate.
    associatedtype AnimatableData : VectorArithmetic
    /// The data to animate.
    var animatableData: Self.AnimatableData { get set }
}

Wir werden hier nicht wirklich in die Tiefe gehen - die Animation geht weit über unser heutiges Thema hinaus. Beachten Sie jedoch, dass SwiftUI über ein VectorArithmetic-Protokoll verfügt und zwischen zwei Snapshots interpolieren kann.

Als Beispiel können Sie sich zwei Werte für die Deckkraft vorstellen: 0 und 1. Die Aufgabe von SwiftUI während der Animation ist es, den Wert der Deckkraft zwischen 0 und 1 entsprechend der Kurve der Animation (linear, leicht nach innen usw.) zu ändern. Indem wir unseren Deckkraftmodifikator immer wieder mit einem interpolierten Wert anwenden, können wir eine sanfte Änderung der Deckkraft sehen. Das ist im Grunde genommen die ganze Grundlage einer animierten Überblendung in Kurzform.

Nun ist es an der Zeit, all das Gelernte anzuwenden. Der erste Schritt besteht darin, einen Picker-Rahmen zu erstellen, von dem aus wir den Übergang einblenden. Wie zuvor können wir Anchor verwenden, um einen Rahmen zu erhalten und diese Daten über die Hierarchie mit Hilfe von Präferenzen und insbesondere durch den Aufruf eines anchorPreference-Modifikators zu übergeben. Da alle Daten, die wir benötigen, der Anchor selbst ist, übergeben wir ihn einfach an die Einstellungen. Der Quellcode enthält bereits alle benötigten Strukturen, aber lassen Sie uns diese zunächst überprüfen:

struct AnchorPreferenceKey<Value>: PreferenceKey {

  static var defaultValue: Anchor<Value>? {
    nil
  }

  static func reduce(value: inout Anchor<Value>?, nextValue: () -> Anchor<Value>?) {
    value = nextValue() ?? value
  }
}

Wie Sie sehen können, ist das nur ein generischer Wrapper, um einen beliebigen Anker durch die Einstellung zu übergeben. Der einzige knifflige Moment hier ist, dass wir den vorherigen Wert verwenden, falls nextValue() null zurückgibt. Der Grund für diesen Code ist, dass die Einstellungen den Wert des Ankers zu verwerfen scheinen, sobald das Layout fertig ist. 

Sobald das erledigt ist, können wir unsere MenuView aktualisieren, um sowohl die Anker-Einstellung anzuwenden als auch den Hintergrund zu aktualisieren, um den Anker für den Übergang zu verwenden, den wir erstellen werden:

var body: some View {
  VStack {
    ...

    ColorSchemePicker(selection: Binding($colorScheme, default: systemColorScheme))
      .anchorPreference(key: AnchorPreferenceKey<CGRect>.self, value: .bounds) { $0 }
      .overrideColorScheme($colorScheme.animation(.default))

    ...
  }
  ...
  .backgroundPreferenceValue(AnchorPreferenceKey<CGRect>.self) { anchor in
    MenuBackground(colorScheme: colorScheme ?? systemColorScheme)
  }
  ...
}

Kurz zur Erläuterung: Wir haben die Einstellung des Ankers eines Pickers auf AnchorPreferenceKey hinzugefügt und den Hintergrund auf backgroundPreferenceValue geändert. Letzteres gibt uns die Möglichkeit, einen zuletzt bekannten Anchor zu lesen und ihn für unseren Übergang zu verwenden.

Erstellen Sie eine neue Datei MenuBackgroundFadeInModifier und fügen Sie den folgenden Code ein

private struct MenuBackgroundFadeInModifier: ViewModifier {

  private let identity: Bool
  private let anchor: Anchor<CGRect>?

  init(identity: Bool, anchor: Anchor<CGRect>?) {
    self.identity = identity
    self.anchor = anchor
  }

  func body(content: Content) -> some View {
    content
  }
}

extension AnyTransition {

  static func menuBackgroundFadeIn(from anchor: Anchor<CGRect>?) -> AnyTransition {
    .asymmetric(
      insertion: .modifier(
        active: MenuBackgroundFadeInModifier(identity: false, anchor: anchor),
        identity: MenuBackgroundFadeInModifier(identity: true, anchor: anchor)
      ),
      removal: .identity
    )
  }
}

Jetzt haben wir ein weiteres neues Element von AnyTransition angewendet: assymetric, mit dem wir verschiedene Übergänge für das Einfügen und Entfernen einer Ansicht festlegen können. Da wir nicht wirklich dieselbe Einblendungsanimation anwenden wollen, lassen wir identity für die Ausblendung.

MenuBackgroundFadeInModifier tut nichts weiter, als den Inhalt des Unmodifiers zurückzugeben. Sie konnten feststellen, dass wir einen Anker von Picker sowie ein bool-Flag übergeben haben, aber wie sieht es mit einem letzten Rahmen aus?

Hier sollten wir den Layout-Prozess von SwiftUI berücksichtigen: Ein Elternteil schlägt einem Kind seine Größe vor und das Kind gibt daraufhin seinen eigenen Gegenvorschlag zurück. Der Elternteil muss diese Wahl respektieren und das Kind entsprechend positionieren. Das bedeutet, dass unser Hintergrund den gesamten verfügbaren Platz einer umfassenden Ansicht erhält und wir diese Größe daher nicht direkt übergeben müssen.

Der nächste Schritt besteht darin, zwischen zwei Bildern zu interpolieren. Aber wie machen wir das? Wir können die übergebene Identität berücksichtigen und, falls dies der Fall ist, diese verwenden. Andernfalls verwenden Sie den gesamten übergebenen Rahmen.

Wie Sie sich erinnern, benötigen wir einen GeometryReader, um Anchor in einen lokalen Koordinatenraum zu konvertieren, da Anchor selbst die Quelle seiner Geometrieeigenschaften kodiert und GeometryReader weiß, wie man sie in einen absoluten Wert umwandelt.

Bevor wir mit der Codierung beginnen, noch ein Hinweis darauf, dass ein Anker optional sein kann. Daher können wir den gesamten verfügbaren Rahmen zurückgeben und ihn in diesem Fall als ungültiges Szenario behandeln:

func body(content: Content) -> some View {
  GeometryReader { proxy in
    let frame = effectiveFrame(using: proxy)

    content
      .frame(width: frame.width, height: frame.height)
      .position(x: frame.midX, y: frame.midY)
  }
}

private func effectiveFrame(using proxy: GeometryProxy) -> CGRect {
  guard let anchor, !identity else {
    return proxy.frame(in: .local)
  }

  return proxy[anchor]
}

Im obigen Codeschnipsel passiert Folgendes: Wenn es sich um einen abgeschlossenen Übergang handelt, erhält die Ansicht die gesamte vorgeschlagene Größe des übergeordneten Elements, so wie wir es oben beschrieben haben. GeometryReader akzeptiert die vorgeschlagene Größe in vollem Umfang und wenn der Übergang beendet ist, erhält auch der Hintergrund die volle Größe des Menüs. Wenn der Übergang begonnen hat und ein Anker verfügbar ist, setzen wir diesen als Basisrahmen.

Sie werden vielleicht fragen: "Ok, aber wir haben das Animatable-Protokoll zwar angesprochen, um Änderungen zu animieren, aber wir verwenden es trotzdem nicht. Woran liegt das?", - und damit liegen Sie völlig richtig. Der Grund dafür, dass wir keine explizite Animation verwenden, ist, dass die Frame- und Positionsmodifikatoren selbst wissen, wie sie animiert werden können. Wir nutzen einfach die vorhandenen Möglichkeiten. 

Sobald wir mit der Einblendung fertig sind, aktualisieren wir unseren Hintergrund in MenuView , um die Animation zu verwenden:

MenuBackground(colorScheme: colorScheme ?? systemColorScheme)
  .transition(.menuBackgroundFadeIn(from: anchor))

Erstellen und ausführen. Puh... Es funktioniert nicht. Die Ansicht ändert einfach animiert die Farbe. Fällt Ihnen etwas ein? 

Wie Sie sehen können, verschwindet unsere Ansicht nie und für SwiftUI ist sie immer gleich. Aber wie zwingen wir sie dazu, jedes Mal, wenn sich das Farbschema ändert, unseren Übergang auszulösen?

An dieser Stelle kommt die explizite Identität ins Spiel. SwiftUI verwendet in hohem Maße die strukturelle Identität, um herauszufinden, ob es sich um dieselbe Ansicht handelt oder nicht. Was aber, wenn wir sagen wollen, dass es sich um eine andere Ansicht handelt, obwohl die Struktur der Ansichten dieselbe ist? Mit dem id-Modifikator können wir genau das tun. Wenn Sie einen anderen Wert übergeben, behandelt SwiftUI die Ansicht als eine völlig neue:

MenuBackground(colorScheme: colorScheme ?? systemColorScheme)
  .transition(.menuBackgroundFadeIn(from: anchor))
  .id(colorScheme ?? systemColorScheme)

Erstellen und ausführen:

Das sieht fast gut aus, außer dass der alte Hintergrund sofort verschwindet. Das ist jedoch nicht gewollt. Der gewünschte Effekt wäre, dass der Hintergrund während der gesamten Animation ohne jegliche Änderung erhalten bleibt. Aber wie können wir das erreichen?

Hier kann uns Animatable helfen: Wir können SwiftUI dazu zwingen, "etwas" Unnützes zu animieren, während wir die Ansicht in der Hierarchie unverändert lassen. Fügen Sie in MenuBackgroundTransition einen weiteren Modifikator hinzu und aktualisieren Sie unseren Übergang:

private struct MenuBackgroundFadeOutModifier: ViewModifier, Animatable {

  var animatableData: Double

  func body(content: Content) -> some View {
    content
  }
}

extension AnyTransition {

  static func menuBackgroundFadeIn(from anchor: Anchor<CGRect>?) -> AnyTransition {
    .asymmetric(
      insertion: .modifier(
        active: MenuBackgroundFadeInModifier(identity: false, anchor: anchor),
        identity: MenuBackgroundFadeInModifier(identity: true, anchor: anchor)
      ),
      removal: .modifier(
        active: MenuBackgroundFadeOutModifier(animatableData: 0),
        identity: MenuBackgroundFadeOutModifier(animatableData: 1)
      )
    )
  }
}

Wie Sie deutlich sehen können, ändern wir nichts und animieren nicht wirklich, aber indem wir unseren Modifikator animierbar machen und einen anderen Wert übergeben, zwingen wir SwiftUI, unseren Ausblendungsmodifikator für die gesamte Animationsdauer anzuwenden, was einen schönen und sauberen Übergang ergibt:

Die endgültige Version des Quellcodes finden Sie hier.

ZUSAMMENFASSUNG

In den beiden Teilen dieses Artikels haben wir uns von Null zu einem vollwertigen und schön animierten Menü vorgearbeitet. Wir haben eine Reihe von Details erläutert, was und warum die ganze Sache so funktioniert, wie sie funktioniert. Außerdem haben wir Sie auf dem Weg zur Beherrschung von SwiftUI begleitet. Eine solche Herangehensweise fehlte in den verschiedenen Tutorials, die Ihnen einzelne Teile und Konzepte von SwiftUI zeigten und die Sie selbst zu einem produktähnlichen Projekt zusammensetzen mussten. Wir hoffen, dass dieser Menüführer alle Ihre Fragen auf dem Weg dorthin beantwortet hat. 

Wie Sie sehen können, wird SwiftUI kompliziert, wenn es um interessante Interaktionen und Animationen geht, aber die Möglichkeiten sind auch sehr mächtig. Es ist nicht einfach, alles auf Anhieb hinzubekommen, aber je mehr Wissen und Erfahrung Sie erwerben, desto einfacher wird das Ganze. 

Wir danken Ihnen für Ihre Geduld. Vielen Dank fürs Lesen und bis zum nächsten Mal!