SwiftUI in den Regalen: Animation. Teil 1

Bild

Kürzlich bin ich auf einen neuen Artikel gestoßen, in dem die Jungs versucht haben, mit SwiftUI ein interessantes Konzept zu reproduzieren. Folgendes haben sie getan:

Bild

Ich habe ihren Code mit Interesse studiert, aber einige Frustrationen erfahren. Nein, nicht in dem Sinne, dass sie etwas falsch gemacht haben, überhaupt nicht. Es ist nur so, dass ich nichts Neues aus ihrem Code gelernt habe. Bei ihrer Implementierung geht es mehr um Kombinieren als um Animation. Und ich habe beschlossen , meinen Lunopark zu erstellen , um meinen Artikel über Animation in SwiftUI zu schreiben. Dabei wurde ungefähr das gleiche Konzept implementiert, aber 100% der Funktionen der integrierten Animation genutzt, auch wenn diese nicht sehr effektiv ist. Lernen - bis zum Ende. Experimentieren - also mit einem Augenzwinkern :) Folgendes

habe ich:


Für eine vollständige Offenlegung des Themas musste ich jedoch ausführlich über die Grundlagen sprechen. Der Text erwies sich als umfangreich, und deshalb habe ich ihn in zwei Artikel aufgeteilt. Hier ist der erste Teil davon - eher ein Tutorial zur Animation im Allgemeinen, das nicht direkt mit der Regenbogenanimation zusammenhängt, auf das ich im nächsten Artikel ausführlich eingehen werde.

In diesem Artikel werde ich auf die Grundlagen eingehen, ohne die Sie in komplexeren Beispielen leicht verwirrt werden können. Vieles, worüber ich in der einen oder anderen Form sprechen werde, wurde bereits in englischsprachigen Artikeln wie dieser Reihe ( 1 , 2 , 3 , 4) beschrieben) Andererseits konzentrierte ich mich weniger auf die Aufzählung der Arbeitsweisen als vielmehr auf die Beschreibung, wie genau dies funktioniert. Und wie immer habe ich viel experimentiert, also beeile ich mich, die interessantesten Ergebnisse zu teilen.

Warnung: Unter der Katze gibt es viele Bilder und Gif-Animationen.

TLDR


Das Projekt ist auf Github verfügbar . Sie können das aktuelle Ergebnis mit Regenbogenanimation in TransitionRainbowView () sehen, aber ich würde mich nicht an Ihre Stelle beeilen, sondern auf den nächsten Artikel warten. Außerdem kämme ich bei der Vorbereitung den Code ein wenig.

In diesem Artikel werden nur die Grundlagen erläutert und nur der Inhalt des Ordners "Basen" beeinflusst.

Einführung


Ich gebe zu, ich wollte diesen Artikel jetzt nicht schreiben. Ich hatte einen Plan, nach dem ein Artikel über Animation der dritte oder sogar der vierte in Folge sein sollte. Ich konnte jedoch nicht widerstehen, ich wollte wirklich einen alternativen Standpunkt bieten.

Ich möchte sofort reservieren. Ich glaube nicht, dass im genannten Artikel Fehler gemacht wurden oder dass der darin verwendete Ansatz falsch ist. Ganz und gar nicht. Es erstellt ein Objektmodell des Prozesses (Animation), das als Reaktion auf das empfangene Signal etwas zu tun beginnt. Für mich zeigt dieser Artikel jedoch höchstwahrscheinlich die Arbeit mit dem Combine-Framework. Ja, dieses Framework ist ein wichtiger Bestandteil von SwiftUI, aber es geht mehr um reaktionsähnlichen Stil im Allgemeinen als um Animation.

Meine Option ist sicherlich nicht eleganter, schneller und einfacher zu warten. Es zeigt jedoch viel besser, was sich unter der Haube von SwiftUI befindet, und tatsächlich war dies der Zweck des Artikels - es zuerst herauszufinden.

Wie ich in einem vorherigen Artikel sagteMit SwiftUI begann ich sofort mit SwiftUI meinen Einstieg in die Welt der mobilen Entwicklung und ignorierte UIKit. Das hat natürlich einen Preis, aber es gibt Vorteile. Ich versuche nicht, gemäß der alten Charta in einem neuen Kloster zu leben. Ehrlich gesagt kenne ich noch keine Chartas, daher habe ich keine Ablehnung der neuen. Aus diesem Grund kann dieser Artikel meines Erachtens nicht nur für Anfänger wie mich von Wert sein, sondern auch für diejenigen, die SwiftUI studieren und bereits Hintergrundinformationen in Form von UIKit-Entwicklungen haben. Es scheint mir, dass vielen Menschen ein frisches Aussehen fehlt. Tun Sie nicht dasselbe und versuchen Sie, ein neues Werkzeug in die alten Zeichnungen einzubauen, sondern ändern Sie Ihre Vision entsprechend den neuen Möglichkeiten.

Wir 1c-Nicks haben dies mit „kontrollierten Formen“ durchgemacht. Dies ist eine Art SwiftUI in der Welt der Einsen, die vor mehr als 10 Jahren passiert ist. Tatsächlich ist die Analogie ziemlich genau, da verwaltete Formulare nur eine neue Möglichkeit sind, eine Schnittstelle zu zeichnen. Er hat jedoch die Client-Server-Interaktion der gesamten Anwendung und insbesondere das Bild der Welt in den Köpfen der Entwickler grundlegend verändert. Das war nicht einfach, ich selbst wollte es ungefähr 5 Jahre lang nicht studieren, weil Ich dachte, dass viele der Möglichkeiten, die dort abgeschnitten wurden, einfach für mich notwendig waren. Wie die Praxis gezeigt hat, ist die Codierung auf verwalteten Formularen jedoch nicht nur möglich, sondern nur erforderlich.

Sprechen wir jedoch nicht mehr darüber. Ich habe einen detaillierten, unabhängigen Leitfaden erhalten, der keine Referenzen oder andere Links zu dem genannten Artikel oder der 1. Vergangenheit enthält. Schritt für Schritt werden wir uns mit den Details, Merkmalen, Prinzipien und Einschränkungen befassen. Gehen.

Animierende Form


Wie Animation im Allgemeinen funktioniert


Die Hauptidee der Animation ist also die Umwandlung einer bestimmten, diskreten Änderung in einen kontinuierlichen Prozess. Zum Beispiel war der Radius des Kreises 100 Einheiten, wurde 50 Einheiten. Ohne Animation erfolgt die Änderung sofort, mit Animation - reibungslos. Wie es funktioniert? Sehr einfach. Für reibungslose Änderungen müssen wir mehrere Werte innerhalb des Segments "Es war ... Es ist geworden" interpolieren. Im Fall des Radius müssen wir mehrere Zwischenkreise mit einem Radius von 98 Einheiten, 95 Einheiten, 90 Einheiten ... 53 Einheiten und schließlich 50 Einheiten zeichnen. SwiftUI kann dies einfach und natürlich tun. Schließen Sie einfach den Code, der diese Änderung vornimmt, in withAnimation {...} ein. Es scheint magisch ... Bis Sie etwas etwas komplizierteres als "Hallo Welt" implementieren möchten.

Kommen wir zu den Beispielen. Das einfachste und verständlichste Objekt für die Animation ist die Animation von Formularen. Shape (ich werde die Struktur weiterhin als Formformprotokoll bezeichnen) in SwiftUI ist eine Struktur mit Parametern, die sich in diese Grenzen einfügen können. Jene. Es ist eine Struktur mit dem Funktionskörper (in rect: CGRect) -> Path. Alles, was die Laufzeit zum Zeichnen dieses Formulars benötigt, ist das Anfordern seines Umrisses (das Ergebnis der Funktion ist ein Objekt vom Typ Pfad, tatsächlich ist es eine Bezier-Kurve) für die erforderliche Größe (angegeben als Funktionsparameter, ein Rechteck vom Typ CGRect).

Form ist eine gespeicherte Struktur. Durch die Initialisierung speichern Sie in den Parametern alles, was Sie zum Zeichnen des Umrisses benötigen. Die Größe der Auswahl für dieses Formular kann sich ändern. Dann müssen Sie lediglich einen neuen Pfadwert für den neuen CGRect-Frame und voila abrufen.

Beginnen wir bereits mit dem Codieren:

struct CircleView: View{
    var radius: CGFloat
    var body: some View{
        Circle()
            .fill(Color.green)
            .frame(height: self.radius * 2)
            .overlay(
                Text("Habra")
                    .font(.largeTitle)
                    .foregroundColor(.gray)
                )

    }
}
struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle()
            .fill(Color.gray)
            .frame(width: self.radius * 2, height: self.radius * 2)
            .overlay(
                Text("Habr")
                    .font(.largeTitle)
                    .foregroundColor(.green)
                )
    }
}
struct CustomCircleTestView: View {
    @State var radius: CGFloat = 50
    var body: some View {
        VStack{
            CircleView(radius: radius)
               .frame(height: 200)
            Slider(value: self.$radius, in: 42...100)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.radius = 50
                }
            }){
                Text("set default radius")
            }
        }
    }
}


Wir haben also einen Kreis (Circle ()), dessen Radius wir mit dem Schieberegler ändern können. Dies geschieht reibungslos, wie Der Schieberegler gibt uns alle Zwischenwerte. Wenn Sie jedoch auf die Schaltfläche „Standardradius festlegen“ klicken, erfolgt die Änderung jedoch nicht sofort, sondern gemäß der Anweisung withAnimation (.linear (Dauer: 1)). Linear, ohne Beschleunigung, 1 Sekunde lang gedehnt. Klasse! Wir haben die Animation gemeistert! Wir sind uns nicht einig :)

Aber was ist, wenn wir unsere eigene Form implementieren und ihre Änderungen animieren wollen? Ist es schwer das zu tun? Lass uns nachsehen.

Ich habe eine Kopie von Circle wie folgt erstellt:

struct CustomCircle: Shape{
    public func path(in rect: CGRect) -> Path{
        let radius = min(rect.width, rect.height) / 2
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        return Path(){path in
            if rect.width > rect.height{
                path.move(to: CGPoint(x: center.x, y: 0))
                let startAngle = Angle(degrees: 270)
                path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle:  startAngle + Angle(degrees: 360), clockwise: false)
            }else{
                path.move(to: CGPoint(x: 0, y: center.y))
                let startAngle = Angle(degrees: 0)
                path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle:  startAngle + Angle(degrees: 360), clockwise: false)
            }
            path.closeSubpath()
        }
    }
}

Der Radius des Kreises wird als halb so klein wie die Breite und Höhe des Randes des uns zugewiesenen Bildschirmbereichs berechnet. Wenn die Breite größer als die Höhe ist, beginnen wir in der Mitte des oberen Randes (Anmerkung 1), beschreiben den vollen Kreis im Uhrzeigersinn (Anmerkung 2) und schließen hier unseren Umriss. Wenn die Höhe größer als die Breite ist, beginnen wir in der Mitte des rechten Randes, beschreiben auch den vollen Kreis im Uhrzeigersinn und schließen die Kontur.

Anmerkung 1
Apple ( ) . , (0, 0), (x, y), x — , y — . .. y. y — . , . 90 , 180 — , 270 — .

Anmerkung 2
1 , “ ” “ ” . Core Graphics (SwiftUI ):
In a flipped coordinate system (the default for UIView drawing methods in iOS), specifying a clockwise arc results in a counterclockwise arc after the transformation is applied.

Lassen Sie uns überprüfen, wie unser neuer Kreis auf Änderungen im withAnimation-Block reagiert:

struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle()
            .fill(Color.gray)
            .frame(width: self.radius * 2, height: self.radius * 2)
            .overlay(
                Text("Habr")
                    .font(.largeTitle)
                    .foregroundColor(.green)
                )
    }
}

struct CustomCircleTestView: View {
    @State var radius: CGFloat = 50
    var body: some View {
        VStack{
                HStack{
                CircleView(radius: radius)
                    .frame(height: 200)
                CustomCircleView(radius: radius)
                    .frame(height: 200)
            }
            Slider(value: self.$radius, in: 42...100)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.radius = 50
                }
            }){
                Text("set default radius")
            }
        }
    }
}


Beeindruckend! Wir haben gelernt, wie wir unsere eigenen Freiformbilder machen und sie animieren können! Es ist so?

Nicht wirklich. Alle Arbeiten hier werden vom Modifikator .frame ausgeführt (Breite: self.radius * 2, Höhe: self.radius * 2). Innerhalb des withAnimation-Blocks {...} ändern wir unsZustandAls Variable sendet sie ein Signal, um CustomCircleView () mit einem neuen Radiuswert neu zu initialisieren. Dieser neue Wert fällt in den Modifikator .frame (), und dieser Modifikator kann bereits Parameteränderungen animieren. Unser CustomCircle () -Formular reagiert darauf mit einer Animation, da es nur von der Größe des dafür ausgewählten Bereichs abhängt. Das Ändern des Bereichs erfolgt mit der Animation (d. H. Allmähliches Interpolieren der Zwischenwerte zwischen den Animationen), daher wird unser Kreis mit derselben Animation gezeichnet.

Lassen Sie uns unsere Form ein wenig vereinfachen (oder noch komplizieren?). Wir werden den Radius nicht basierend auf der Größe der verfügbaren Fläche berechnen, sondern den Radius in der fertigen Form übertragen, d. H. Machen Sie es zu einem gespeicherten Strukturparameter.

struct CustomCircle: Shape{
    var radius: CGFloat
    public func path(in rect: CGRect) -> Path{
        //let radius = min(rect.width, rect.height) / 2
...
    }
}

struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle(radius: radius)
            .fill(Color.gray)
            //.frame(height: self.radius * 2)
...
    }
}


Nun, Magie ist unwiederbringlich verloren.

Wir haben den Modifikator frame () aus unserem CustomCircleView () ausgeschlossen und die Verantwortung für die Größe des Kreises auf die Form selbst verlagert. Die Animation ist verschwunden. Es spielt jedoch keine Rolle, eine Form zu lehren, um Änderungen in ihren Parametern zu animieren, ist nicht allzu schwierig. Dazu müssen Sie die Anforderungen des Animatable-Protokolls implementieren:

struct CustomCircle: Shape, Animatable{
    var animatableData: CGFloat{
         get{
             radius
         }
         set{
            print("new radius is \(newValue)")
            radius = newValue
         }
     }
    var radius: CGFloat
    public func path(in rect: CGRect) -> Path{
	...
}


Voila! Die Magie ist wieder da!

Und jetzt können wir sicher sagen, dass unser Formular wirklich animiert ist - es kann Änderungen in seinen Parametern mit Animation widerspiegeln. Wir haben dem System ein Fenster gegeben, in dem es die für die Animation benötigten interpolierten Werte überfüllen kann. Wenn es ein solches Fenster gibt, werden die Änderungen animiert. Wenn dies nicht der Fall ist, finden die Änderungen ohne Animation statt, d.h. sofort. Nichts kompliziertes, oder?

AnimatableModifier


So animieren Sie Änderungen in einer Ansicht


Aber gehen wir direkt zu Ansicht. Angenommen, wir möchten die Position eines Elements in einem Container animieren. In unserem Fall handelt es sich um ein einfaches Rechteck mit grüner Farbe und einer Breite von 10 Einheiten. Wir werden seine horizontale Position animieren.

struct SimpleView: View{
    @State var position: CGFloat = 0
    var body: some View{
        VStack{
            ZStack{
                Rectangle()
                    .fill(Color.gray)
                BorderView(position: position)
            }
            Slider(value: self.$position, in: 0...1)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.position = 0
                }
            }){
                Text("set to 0")
            }
        }
    }
}

struct BorderView: View,  Animatable{
    public var animatableData: CGFloat {
        get {
            print("Reding position: \(position)")
            return self.position
        }
        set {
            self.position = newValue
            print("setting position: \(position)")
        }
    }
    let borderWidth: CGFloat
    init(position: CGFloat, borderWidth: CGFloat = 10){
        self.position = position
        self.borderWidth = borderWidth
        print("BorderView init")
    }
    var position: CGFloat
    var body: some View{
        GeometryReader{geometry in
            Rectangle()
                .fill(Color.green)
                .frame(width: self.borderWidth)
                .offset(x: self.getXOffset(inSize: geometry.size), y: 0)
                // .borderIn(position: position)
        }
    }
    func getXOffset(inSize: CGSize) -> CGFloat{
        print("calculating position: \(position)")
        return -inSize.width / 2 + inSize.width * position
    }
}


Klasse! Funktioniert! Jetzt wissen wir alles über Animation!

Nicht wirklich. Wenn Sie sich die Konsole ansehen, sehen wir Folgendes:
BorderView Init-
Berechnungsposition: 0.4595176577568054
BorderView Init-
Berechnungsposition: 0.468130886554718
BorderView Init-
Berechnungsposition: 0.0

Erstens bewirkt jede Änderung des Positionswerts mithilfe des Schiebereglers, dass BorderView mit dem neuen Wert neu initialisiert wird. Aus diesem Grund sehen wir eine sanfte Bewegung der grünen Linie nach dem Schieberegler. Der Schieberegler meldet einfach sehr oft eine Änderung der Variablen und sieht aus wie eine Animation, ist es aber nicht. Die Verwendung des Schiebereglers ist sehr praktisch, wenn Sie Animationen debuggen. Sie können es verwenden, um einige Übergangszustände zu verfolgen.

Zweitens sehen wir, dass die Berechnungsposition einfach gleich 0 wurde und keine Zwischenprotokolle, wie dies bei der korrekten Animation des Kreises der Fall war. Warum?

Die Sache ist, wie im vorherigen Beispiel, im Modifikator. Dieses Mal erhält der Modifikator .offset () den neuen Einrückungswert und animiert die Änderung selbst. Jene. Tatsächlich ist es nicht die Änderung des Positionsparameters, die animiert werden soll, sondern die horizontale Änderung des Einzugs im davon abgeleiteten Modifikator .offset (). In diesem Fall ist dies ein harmloser Ersatz, das Ergebnis ist das gleiche. Aber da sie gekommen sind, wollen wir tiefer graben. Lassen Sie uns unseren eigenen Modifikator erstellen, der die Position (von 0 bis 1) am Eingang erhält, die Größe des verfügbaren Bereichs empfängt und den Einzug berechnet.

struct BorderPosition: ViewModifier{
    var position: CGFloat
    func body(content: Content) -> some View {
        GeometryReader{geometry in
            content
            .offset(x: self.getXOffset(inSize: geometry.size), y: 0)
            .animation(nil)
        }
    }
    func getXOffset(inSize: CGSize) -> CGFloat{
        let offset = -inSize.width / 2 + inSize.width * position
        print("at position  \(position) offset is \(offset)")
        return offset
    }
}

extension View{
    func borderIn(position: CGFloat) -> some View{
        self.modifier(BorderPosition(position: position))
    }
}

In der ursprünglichen BorderView wird der GeometryReader nicht mehr benötigt, ebenso wie die Funktion zum Berechnen des Einzugs:

struct BorderView: View,  Animatable{
    ...
    var body: some View{
            Rectangle()
                .fill(Color.green)
                .frame(width: self.borderWidth)
                .borderIn(position: position)
    }
}


Ja, wir verwenden immer noch den Modifikator .offset () in unserem Modifikator, aber danach haben wir den Modifikator .animation (nil) hinzugefügt, der unsere eigene Offset-Animation blockiert. Ich verstehe, dass Sie zu diesem Zeitpunkt entscheiden können, dass es ausreicht, dieses Schloss zu entfernen, aber dann werden wir der Wahrheit nicht auf den Grund gehen. Und die Wahrheit ist, dass unser Trick mit animatableData für BorderView nicht funktioniert. Wenn Sie sich die Dokumentation zum Animatable- Protokoll ansehen , werden Sie feststellen, dass die Implementierung dieses Protokolls nur für AnimatableModifier, GeometryEffect und Shape unterstützt wird. Ansicht ist nicht unter ihnen.

Der richtige Ansatz besteht darin, Änderungen zu animieren


Der Ansatz selbst war falsch, als wir View aufforderten, einige Änderungen zu animieren. Für die Ansicht können Sie nicht denselben Ansatz wie für Formulare verwenden. Stattdessen muss die Animation in jeden Modifikator eingebettet werden. Die meisten integrierten Modifikatoren unterstützen bereits sofort einsatzbereite Animationen. Wenn Sie eine Animation für Ihre eigenen Modifikatoren wünschen, können Sie das AnimatableModifier-Protokoll anstelle von ViewModifier verwenden. Und dort können Sie das Gleiche wie beim Animieren von Formänderungen implementieren, wie wir es oben getan haben.

struct BorderPosition: AnimatableModifier {
    var position: CGFloat
    let startDate: Date = Date()
    public var animatableData: CGFloat {
        get {
            print("reading position: \(position) at time \(Date().timeIntervalSince(startDate))")
            return position
        }
        set {
            position = newValue
            print("setting position: \(position) at time \(Date().timeIntervalSince(startDate))")
        }
    }
    func body(content: Content) -> some View {
...
    }
    ...
}


Jetzt ist alles richtig. Nachrichten in der Konsole helfen zu verstehen, dass unsere Animation wirklich funktioniert, und .animation (nil) im Modifikator stört sie überhaupt nicht. Aber lassen Sie uns trotzdem genau herausfinden, wie es funktioniert.

Zunächst müssen Sie verstehen, was ein Modifikator ist.


Hier haben wir einen Blick. Wie ich im vorherigen Teil sagte, ist dies eine Struktur mit gespeicherten Parametern und Montageanweisungen. Diese Anweisung enthält im Großen und Ganzen keine Abfolge von Aktionen. Dies ist der übliche Code, den wir in einem nicht deklarativen Stil schreiben, sondern eine einfache Liste. Es listet die andere Ansicht, die auf sie angewendeten Modifikatoren und die Container auf, in denen sie enthalten sind. Wir sind noch nicht an Containern interessiert, aber lassen Sie uns mehr über Modifikatoren sprechen.

Ein Modifikator ist wiederum eine Struktur mit gespeicherten Parametern und Anweisungen zur Ansichtsverarbeitung. Dies ist eigentlich die gleiche Anweisung wie in der Ansicht - wir können andere Modifikatoren, Container (zum Beispiel habe ich den GeometryReader etwas höher verwendet) und sogar andere Ansichten verwenden. Wir haben jedoch nur eingehende Inhalte und müssen diese mithilfe dieser Anweisung irgendwie ändern. Modifikatorparameter sind Teil der Anweisung. Am interessantesten ist jedoch, dass sie gespeichert werden.

In einem früheren Artikel habe ich gesagt, dass die Anweisung selbst nicht gespeichert ist und jedes Mal ausgelöst wird, nachdem die Ansicht aktualisiert wurde. Alles ist so, aber es gibt eine Nuance. Als Ergebnis der Arbeit dieser Anweisung erhalten wir, wie ich bereits sagte, kein Bild - es war eine Vereinfachung. Modifikatoren verschwinden nach der Ausführung dieser Anweisung nicht. Sie bleiben so, solange die übergeordnete Ansicht vorhanden ist.

Einige primitive Analogien


Wie würden wir eine Tabelle deklarativ beschreiben? Nun, wir würden 4 Beine und eine Arbeitsplatte auflisten. Sie würden sie zu einer Art Behälter kombinieren und mit Hilfe einiger Modifikatoren vorschreiben, wie sie aneinander befestigt werden. Zum Beispiel würde jedes Bein die Ausrichtung in Bezug auf die Arbeitsplatte und die Position angeben - welches Bein an welcher Ecke befestigt ist. Ja, wir können die Anweisungen nach dem Zusammenbau wegwerfen, aber die Nägel bleiben im Tisch. So sind die Modifikatoren. Am Ausgang der Körperfunktion haben wir keinen Tisch. Mit body erstellen wir Tabellenelemente (Ansicht) und Verbindungselemente (Modifikatoren) und legen alles in Schubladen an. Der Tisch selbst wird von einem Roboter zusammengebaut. Welche Befestigungselemente Sie in eine Schachtel an jedem Bein stecken, erhalten Sie einen solchen Tisch.

Die Funktion .modifier (BorderPosition (position: position)), mit der wir die BorderPosition-Struktur in einen Modifier umgewandelt haben, setzt nur eine zusätzliche Schraube in der Schublade am Tischbein. Die BorderPosition-Struktur ist diese Schraube. Das Rendern nimmt zum Zeitpunkt des Renderns dieses Feld, nimmt ein Bein heraus (in unserem Fall Rectangle ()) und ruft nacheinander alle Modifikatoren aus der Liste mit den darin gespeicherten Werten ab. Die Körperfunktion jedes Modifikators ist eine Anweisung, wie ein Bein mit dieser Schraube an eine Tischplatte geschraubt wird, und die Struktur selbst mit gespeicherten Eigenschaften, dies ist diese Schraube.

Warum ist es wichtig, dies im Kontext der Animation zu verstehen? Mit der Animation können Sie die Parameter eines Modifikators ändern, ohne die anderen zu beeinflussen, und das Bild dann erneut rendern. Wenn Sie dasselbe tun, indem Sie einige ändern@StateParameter - Dies führt zu einer Neuinitialisierung der verschachtelten Ansicht, der Modifikatorstrukturen usw. entlang der gesamten Kette. Aber die Animation ist nicht.

Wenn wir den Wert der Position ändern, wenn wir eine Taste drücken, ändert sich dies tatsächlich. Bis zum Ende. In der Variablen selbst sind keine Zwischenzustände gespeichert, was über den Modifikator nicht gesagt werden kann. Für jedes neue Bild ändern sich die Werte der Modifikatorparameter entsprechend dem Fortschritt der aktuellen Animation. Wenn die Animation 1 Sekunde dauert, ändert sich alle 1/60 Sekunde (das iPhone zeigt genau die Anzahl der Bilder pro Sekunde) der animatableData-Wert im Modifikator und wird vom Rendering zum Rendern gelesen. Nach einer weiteren 1/60 Sekunde wird er angezeigt erneut geändert und vom Render erneut gelesen.

Was charakteristisch ist, wir erhalten zuerst den Endzustand der gesamten Ansicht, merken uns das und erst dann beginnt der Animationsmechanismus, die interpolierten Positionswerte in den Modifikator zu übertragen. Der Ausgangszustand wird nirgendwo gespeichert. Irgendwo im Darm von SwiftUI wird nur der Unterschied zwischen dem Anfangs- und dem Endzustand gespeichert. Diese Differenz wird jedes Mal mit dem Bruchteil der verstrichenen Zeit multipliziert. Auf diese Weise wird der interpolierte Wert berechnet, der anschließend in animatableData eingesetzt wird.

Differenz =

Stahl
- war aktueller Wert = Stahl - Differenz * (1 - verstrichene Zeit) verstrichene Zeit = Zeit ab StartAnimationen / DauerAnimationen Der

aktuelle Wert muss so oft berechnet werden, wie die Anzahl der Frames angezeigt werden muss.

Warum wird es nicht explizit verwendet? Tatsache ist, dass SwiftUI den Ausgangszustand nicht speichert. Es wird nur der Unterschied gespeichert: Im Falle eines Fehlers können Sie die Animation einfach ausschalten und zum aktuellen Status „Werden“ wechseln.

Mit diesem Ansatz können Sie die Animation reversibel machen. Angenommen, irgendwo in der Mitte einer Animation hat der Benutzer erneut eine Taste gedrückt, und wir haben den Wert derselben Variablen erneut geändert. In diesem Fall müssen wir nur "Aktuell" in der Animation zum Zeitpunkt der neuen Änderung als "Es" nehmen, sich an den neuen Unterschied erinnern und eine neue Animation basierend auf dem neuen "Wurde" und dem neuen "Unterschied" starten. . Ja, tatsächlich sind diese Übergänge von einer Animation zur anderen etwas schwieriger zu simulieren, aber die Bedeutung ist meines Erachtens verständlich.

Interessant ist, dass die Animation in jedem Frame nach dem aktuellen Wert im Modifikator fragt (mithilfe eines Getters). Dies ist, wie Sie den Serviceaufzeichnungen im Protokoll entnehmen können, für den Status „Stahl“ verantwortlich. Dann setzen wir mit dem Setter den neuen Status, der für diesen Frame aktuell ist. Danach wird für den nächsten Frame erneut der aktuelle Wert vom Modifikator angefordert - und er wird wieder "geworden", d. H. Der endgültige Wert, zu dem sich die Animation bewegt. Es ist wahrscheinlich, dass Kopien von Modifikatorstrukturen für die Animation verwendet werden und ein Getter einer Struktur (ein realer Modifikator der tatsächlichen Ansicht) verwendet wird, um den Wert „Stahl“ zu erhalten, und ein Setter einer anderen (ein temporärer Modifikator, der für die Animation verwendet wird). Ich habe mir keine Möglichkeit ausgedacht, dies sicherzustellen, aber durch indirekte Angaben sieht alles einfach so aus. So oder so,Änderungen innerhalb der Animation wirken sich nicht auf den gespeicherten Wert der Modifikatorstruktur der aktuellen Ansicht aus. Wenn Sie Ideen haben, wie Sie genau herausfinden können, was genau mit dem Getter und Setter passiert, schreiben Sie darüber in den Kommentaren. Ich werde den Artikel aktualisieren.

Mehrere Parameter


Bis zu diesem Moment hatten wir nur einen Parameter für die Animation. Die Frage kann sich stellen, aber was ist, wenn mehr als ein Parameter an den Modifikator übergeben wird? Und wenn beide gleichzeitig animiert werden müssen? So geht es zum Beispiel mit dem Frame-Modifikator (Breite: Höhe :). Schließlich können wir gleichzeitig sowohl die Breite als auch die Höhe dieser Ansicht ändern, und wir möchten, dass die Änderung in einer Animation erfolgt. Wie geht das? Immerhin ist der AnimatableData-Parameter einer. Was soll er ersetzen?

Wenn Sie schauen, hat Apple nur eine Anforderung für animatableData. Der Datentyp, den Sie ersetzen, muss dem VectorArithmetic-Protokoll entsprechen. Für dieses Protokoll muss das Objekt die minimalen arithmetischen Operationen sicherstellen, die erforderlich sind, um ein Segment mit zwei Werten bilden zu können, und die Punkte innerhalb dieses Segments interpolieren. Die dafür notwendigen Operationen sind Addition, Subtraktion und Multiplikation. Die Schwierigkeit besteht darin, dass wir diese Operationen mit einem einzelnen Objekt ausführen müssen, in dem mehrere Parameter gespeichert sind. Jene. Wir müssen die gesamte Liste unserer Parameter in einen Container packen, der ein Vektor sein wird. Apple bietet ein solches Objekt sofort an und bietet uns an, eine schlüsselfertige Lösung für nicht sehr schwierige Fälle zu verwenden. Es heißt AnimatablePair.

Lassen Sie uns die Aufgabe ein wenig ändern. Wir brauchen einen neuen Modifikator, der nicht nur den grünen Balken verschiebt, sondern auch seine Höhe ändert. Dies sind zwei unabhängige Modifikatorparameter. Ich werde nicht den vollständigen Code aller Änderungen angeben, die vorgenommen werden müssen. Sie können ihn auf dem Github in der SimpleBorderMove-Datei sehen. Ich werde nur den Modifikator selbst zeigen:

struct TwoParameterBorder: AnimatableModifier {
    var position: CGFloat
    var height: CGFloat
    let startDate: Date = Date()
    public var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get {
           print("animation read position: \(position), height: \(height)")
           return AnimatablePair(position, height)
        }
        set {
            self.position = newValue.first
            print("animating position at \(position)")
            self.height = newValue.second
            print("animating height at \(height)")
        }
    }
    init(position: CGFloat, height: CGFloat){
        self.position = position
        self.height = height
    }
    func body(content: Content) -> some View {
        GeometryReader{geometry in
            content
                .animation(nil)
                .offset(x: -geometry.size.width / 2 + geometry.size.width * self.position, y: 0)
                .frame(height: self.height * (geometry.size.height - 20) + 20)
        }
    }
}


Ich habe einen weiteren Schieberegler und eine Schaltfläche zum zufälligen Ändern beider Parameter in der übergeordneten Ansicht von SimpleView hinzugefügt, aber es gibt nichts Interessantes. Für den vollständigen Code sind Sie also im Github willkommen.

Alles funktioniert, wir erhalten wirklich eine konsistente Änderung der Parameterpaare, die im AnimatablePair-Tupel enthalten sind. Nicht schlecht.

Nichts verwirrt an dieser Implementierung? Persönlich habe ich mich angespannt, als ich diesen Entwurf sah:

        
self.position = newValue.first
self.height = newValue.second

Ich habe nirgendwo angegeben, welcher dieser Parameter zuerst und welcher an zweiter Stelle stehen soll. Wie entscheidet SwiftUI, welcher Wert zuerst und welcher Wert an zweiter Stelle eingegeben werden soll? Stimmen die Namen der Funktionsparameter nicht mit den Namen der Strukturattribute überein?

Die erste Idee war die Reihenfolge der Attribute in den Parametern der Funktion und deren Typen, wie dies bei @EnvironmentObject der Fall ist. Dort geben wir einfach die Werte in das Feld ein, ohne ihnen Beschriftungen zuzuweisen, und holen sie dann heraus, auch ohne Beschriftungen anzugeben. Dort ist der Typ wichtig und innerhalb eines Typs die Reihenfolge. In welcher Reihenfolge sie in die Schachtel legen, auf die gleiche Weise und bekommen es. Ich versuchte eine andere Reihenfolge der Argumente der Funktion, die Reihenfolge der Argumente zum Initialisieren der Struktur, die Reihenfolge der Attribute der Struktur selbst, schlug meinen Kopf im Allgemeinen gegen die Wand, konnte SwiftUI jedoch nicht verwirren, so dass es begann, die Position mit Höhenwerten zu animieren und umgekehrt.

Dann dämmerte es mir. Ich selbst gebe an, welcher Parameter der erste und welcher zweite im Getter sein wird. SwiftUI muss nicht genau wissen, wie wir diese Struktur initialisieren. Es kann den animatableData-Wert vor der Änderung abrufen, ihn nach der Änderung abrufen, die Differenz zwischen ihnen berechnen und dieselbe Differenz, skaliert proportional zum verstrichenen Zeitintervall, an unseren Setter zurückgeben. Im Allgemeinen muss nichts über den Wert selbst in AnimatableData bekannt sein. Und wenn Sie die Reihenfolge der Variablen in zwei benachbarten Zeilen nicht verwechseln, ist alles in Ordnung, egal wie kompliziert die Struktur des restlichen Codes ist.

Aber lass es uns überprüfen. Schließlich können wir unseren eigenen Container-Vektor erstellen (oh, ich liebe es, unsere eigene Implementierung vorhandener Objekte zu erstellen, das haben Sie vielleicht aus einem früheren Artikel bemerkt).



Wir beschreiben die Elementarstruktur, deklarieren die Unterstützung für das VectorArithmetic-Protokoll, öffnen den Fehler, dass das Protokoll nicht konform ist, klicken auf Fix und erhalten die Deklaration aller erforderlichen Funktionen und berechneten Parameter. Es bleibt nur, um sie zu füllen.

Auf die gleiche Weise füllen wir unser Objekt mit den erforderlichen Methoden für das AdditiveArithmetic-Protokoll (VectorArithmetic enthält dessen Unterstützung).

struct MyAnimatableVector: VectorArithmetic{
    static func - (lhs: MyAnimatableVector, rhs: MyAnimatableVector) -> MyAnimatableVector {
        MyAnimatableVector(position: lhs.position - rhs.position, height: lhs.height - rhs.height)
    }
    
    static func + (lhs: MyAnimatableVector, rhs: MyAnimatableVector) -> MyAnimatableVector {
        MyAnimatableVector(position: lhs.position + rhs.position, height: lhs.height + rhs.height)
    }
    
    mutating func scale(by rhs: Double) {
        self.position = self.position * CGFloat(rhs)
        self.height = self.height * CGFloat(rhs)
    }
    
    var magnitudeSquared: Double{
         Double(self.position * self.position) + Double(self.height * self.height)
    }
    
    static var zero: MyAnimatableVector{
        MyAnimatableVector(position: 0, height: 0)
    }
    
    var position: CGFloat
    var height: CGFloat
}

  • Ich denke warum wir + und - offensichtlich brauchen.
  • Skalierung ist eine Funktion der Skalierung. Wir nehmen den Unterschied „Es war - es ist geworden“ und multiplizieren ihn mit der aktuellen Stufe der Animation (von 0 bis 1). "Es wurde + Differenz * (1 - Stufe)" und es wird einen aktuellen Wert geben, den wir in animatableData ablegen sollten
  • Null wird wahrscheinlich benötigt, um neue Objekte zu initialisieren, deren Werte für die Animation verwendet werden. Die Animation verwendet zu Beginn .zero, aber ich konnte nicht genau herausfinden, wie. Ich halte dies jedoch nicht für wichtig.
  • MagnitudeSquared ist ein Skalarprodukt eines bestimmten Vektors mit sich selbst. Für den zweidimensionalen Raum bedeutet dies die Länge des Vektors im Quadrat. Dies wird wahrscheinlich verwendet, um zwei Objekte nicht elementweise, sondern als Ganzes miteinander vergleichen zu können. Es scheint nicht für Animationszwecke verwendet zu werden.

Im Allgemeinen sind die Funktionen "- =" "+ =" ebenfalls in der Protokollunterstützung enthalten, können jedoch für die Struktur automatisch in dieser Form generiert werden

    static func -= (lhs: inout MyAnimatableVector, rhs: MyAnimatableVector) {
        lhs = lhs - rhs
    }

    static func += (lhs: inout MyAnimatableVector, rhs: MyAnimatableVector) {
        lhs = lhs + rhs
    }


Der Klarheit halber habe ich all diese Logik in Form eines Diagramms dargestellt. Das Bild ist anklickbar. Was wir während der Animation erhalten, wird rot hervorgehoben - bei jedem nächsten Tick (1/60 Sekunde) gibt der Timer einen neuen Wert von t an, und wir erhalten im Setter unseres Modifikators einen neuen Wert von animatableData. So funktioniert Animation unter der Haube. Gleichzeitig ist es wichtig zu verstehen, dass ein Modifikator eine gespeicherte Struktur ist und eine Kopie des aktuellen Modifikators mit einem neuen, aktuellen Status zum Anzeigen der Animation verwendet wird.





Warum AnimatableData nur eine Struktur sein kann


Es gibt noch einen Punkt. Sie können Klassen nicht als AnimatableData-Objekt verwenden. Formal können Sie für eine Klasse alle erforderlichen Methoden des entsprechenden Protokolls beschreiben, dies wird jedoch nicht erfolgreich sein, und hier ist der Grund dafür. Wie Sie wissen, ist eine Klasse ein Referenzdatentyp und eine Struktur ein wertebasierter Datentyp. Wenn Sie eine Variable basierend auf einer anderen erstellen, kopieren Sie im Fall einer Klasse einen Link zu diesem Objekt, und im Fall einer Struktur erstellen Sie ein neues Objekt basierend auf den Werten der vorhandenen. Hier ist ein kleines Beispiel, das diesen Unterschied veranschaulicht:

    struct TestStruct{
        var value: CGFloat
        mutating func scaled(by: CGFloat){
            self.value = self.value * by
        }
    }
    class TestClass{
        var value: CGFloat
        func scaled(by: CGFloat){
             self.value = self.value * by
        }
        init(value: CGFloat){
            self.value = value
        }
    }
        var stA = TestStruct(value: 5)
        var stB = stA
        stB.scaled(by: 2)
        print("structs: a = \(stA.value), b = \(stB.value))") //structs: a = 5.0, b = 10.0)
        var clA = TestClass(value: 5)
        var clB = clA
        clB.scaled(by: 2)
        print("classes: a = \(clA.value), b = \(clB.value))") //classes: a = 10.0, b = 10.0)

Mit Animation passiert genau das Gleiche. Wir haben ein AnimatableData-Objekt, das den Unterschied zwischen "war" und "wurde" darstellt. Wir müssen einen Teil dieser Differenz berechnen, um sie auf dem Bildschirm wiederzugeben. Dazu müssen wir diesen Unterschied kopieren und mit einer Zahl multiplizieren, die die aktuelle Stufe der Animation darstellt. Im Fall der Struktur wirkt sich dies nicht auf den Unterschied selbst aus, im Fall der Klasse jedoch. Der erste Rahmen, den wir zeichnen, ist der Zustand „war“. Dazu müssen wir Stahl + Differenz * Aktuelle Stufe - Differenz berechnen. Im Fall der Klasse multiplizieren wir im ersten Rahmen die Differenz mit 0, setzen sie auf Null, und alle nachfolgenden Rahmen werden so gezeichnet, dass die Differenz = 0 ist, d.h. Die Animation scheint korrekt gezeichnet zu sein, aber tatsächlich sehen wir einen sofortigen Übergang von einem Zustand in einen anderen, als ob es keine Animation gäbe.

Sie können wahrscheinlich eine Art Low-Level-Code schreiben, der neue Speicheradressen für das Multiplikationsergebnis erstellt - aber warum? Sie können einfach Strukturen verwenden - sie werden dafür erstellt.

Für diejenigen, die genau verstehen möchten, wie SwiftUI Zwischenwerte berechnet, durch welche Vorgänge und zu welchem ​​Zeitpunkt Nachrichten in die Konsole des Projekts übertragen werden . Außerdem habe ich dort 0,1 Sekunden Schlaf eingefügt, um ressourcenintensive Berechnungen in die Animation zu simulieren. Viel Spaß :)

Bildschirmanimation: .transition ()


Bis zu diesem Punkt haben wir darüber gesprochen, eine Änderung eines Werts zu animieren, der an einen Modifikator oder ein Formular übergeben wurde. Dies sind ziemlich mächtige Werkzeuge. Es gibt jedoch noch ein anderes Tool, das ebenfalls Animationen verwendet - dies ist die Animation des Auftretens und Verschwindens der Ansicht.

Im letzten Artikel haben wir darüber gesprochen, dass dies im deklarativen Stil von if-else überhaupt keine Kontrolle über den Codefluss zur Laufzeit ist, sondern eine Ansicht von Schrödinger. Dies ist ein Container, der zwei Ansichten gleichzeitig enthält und entscheidet, welche gemäß einer bestimmten Bedingung angezeigt werden soll. Wenn Sie den else-Block verpassen, wird anstelle der zweiten Ansicht EmptyView angezeigt.

Das Umschalten zwischen den beiden Ansichten kann auch animiert werden. Hierfür wird der Modifikator .transition () verwendet.

struct TransitionView: View {
    let views: [AnyView] = [AnyView(CustomCircleTestView()), AnyView(SimpleBorderMove())]
    @State var currentViewInd = 0
    var body: some View {
        VStack{
            Spacer()
            ZStack{
                ForEach(views.indices, id: \.self){(ind: Int) in
                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                        }
                    }
                }
            }
            HStack{
                ForEach(views.indices, id: \.self){(ind: Int) in
                    RoundedRectangle(cornerRadius: 10)
                        .fill(ind == self.currentViewInd ? Color.green : Color.gray)
                        .overlay(
                            Text("\(ind + Int(1))"))
                        .onTapGesture{
                            withAnimation{
                                self.currentViewInd = ind
                            }
                    }
                }
            }
                .frame(height: 50)
            Spacer()
        }
    }
}

Mal sehen, wie es funktioniert. Zunächst haben wir bereits in der Initialisierungsphase der übergeordneten Ansicht mehrere Ansichten erstellt und im Array platziert. Das Array ist vom Typ AnyView, da die Elemente des Arrays denselben Typ haben müssen, andernfalls können sie in ForEach nicht verwendet werden. Undurchsichtiger Ergebnistyp aus dem vorherigen Artikel , erinnerst du dich?

Als nächstes haben wir die Aufzählung der Indizes dieses Arrays vorgeschrieben und für jeden von ihnen die Ansicht anhand dieses Index angezeigt. Wir sind gezwungen, dies zu tun und nicht sofort über View zu iterieren, da wir für die Arbeit mit ForEach jedem Element einen internen Bezeichner zuweisen müssen, damit SwiftUI den Inhalt der Sammlung durchlaufen kann. Alternativ müssten wir in jeder Ansicht eine Proxy-ID erstellen, aber warum, wenn Indizes verwendet werden können?

Wir verpacken jede Ansicht aus der Sammlung in einen Zustand und zeigen sie nur an, wenn sie aktiv ist. Das if-else-Konstrukt kann hier jedoch nicht existieren. Der Compiler übernimmt es zur Steuerung des Flusses. Daher schließen wir dies alles in Group ein, damit der Compiler genau versteht, was es ist. View, genauer gesagt, Anweisungen für ViewBuilder zum Erstellen eines optionalen Containers ConditionalContent <View1, View2>.

Wenn Sie nun den Wert von currentViewInd ändern, blendet SwiftUI die vorherige aktive Ansicht aus und zeigt die aktuelle an. Wie gefällt Ihnen diese Navigation in der Anwendung?


Alles, was noch getan werden muss, ist, die aktuelle ViewInd-Änderung in den withAnimation-Wrapper einzufügen, und das Wechseln zwischen Fenstern wird reibungslos.

Fügen Sie den Modifikator .transition hinzu und geben Sie als Parameter .scale an. Dadurch wird die Animation des Erscheinungsbilds und Verschwindens jeder dieser Ansichten unterschiedlich - wobei die Skalierung anstelle der von der Standard-SwiftUI verwendeten Transparenz verwendet wird.

                ForEach(views.indices, id: \.self){(ind: Int) in
                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                                .transition(.scale)
                        }
                    }
                }


Beachten Sie, dass die Ansicht mit derselben Animation angezeigt und ausgeblendet wird. Nur das Verschwinden wird in umgekehrter Reihenfolge gescrollt. Tatsächlich können wir Animationen sowohl für das Erscheinungsbild als auch für das Verschwinden einer Ansicht individuell zuweisen. Hierfür wird ein asymmetrischer Übergang verwendet.

                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                               .transition(.asymmetric(
                                    insertion: insertion: AnyTransition.scale(scale: 0.1, anchor: .leading).combined(with: .opacity),
                                    removal: .move(edge: .trailing)))
                        }
                    }


Die gleiche .scale-Animation wird auf dem Bildschirm angezeigt, aber jetzt haben wir die Parameter für ihre Verwendung angegeben. Es beginnt nicht mit einer Größe von Null (Punkt), sondern mit einer Größe von 0,1 gegenüber der üblichen. Die Startposition des kleinen Fensters befindet sich nicht in der Mitte des Bildschirms, sondern ist zum linken Rand verschoben. Darüber hinaus ist nicht ein Übergang für das Erscheinungsbild verantwortlich, sondern zwei. Sie können mit .combined (mit :) kombiniert werden. In diesem Fall haben wir Transparenz hinzugefügt.

Das Verschwinden der Ansicht wird jetzt durch eine andere Animation gerendert - über den rechten Bildschirmrand. Ich habe die Animation etwas langsamer gemacht, damit Sie sie sich ansehen können.

Und wie immer kann ich es kaum erwarten, meine eigene Version der Transitanimation zu schreiben. Dies ist noch einfacher als animierte Formulare oder Modifikatoren.

struct SpinTransitionModifier: ViewModifier {
    let angle: Double
    let anchor: UnitPoint
    func body(content: Content) -> some View {
        content
            .rotationEffect(Angle(degrees: angle), anchor: anchor)
            .clipped()
    }
}

extension AnyTransition {
    static func spinIn(anchor: UnitPoint) -> AnyTransition {
        .modifier(
            active: SpinTransitionModifier(angle: -90, anchor: anchor),
            identity: SpinTransitionModifier(angle: 0, anchor: anchor))
    }
    static func spinOut(anchor: UnitPoint) -> AnyTransition {
        .modifier(
            active: SpinTransitionModifier(angle: 90, anchor: anchor),
            identity: SpinTransitionModifier(angle: 0, anchor: anchor))
    }
}


Zunächst schreiben wir den üblichen Modifikator, in dem wir eine bestimmte Zahl übertragen - den Drehwinkel in Grad sowie den Punkt, zu dem diese Drehung erfolgt. Anschließend erweitern wir den AnyTransition-Typ um zwei Funktionen. Es hätte einer sein können, aber es schien mir bequemer. Ich fand es einfacher, jedem von ihnen sprechende Namen zuzuweisen, als die Rotationsgrade direkt in der Ansicht selbst zu steuern.

Der AnyTransition-Typ verfügt über eine statische Modifikatormethode, an die wir zwei Modifikatoren übergeben, und wir erhalten ein AnyTransition-Objekt, das einen reibungslosen Übergang von einem Zustand in einen anderen beschreibt. Identität ist der normale Statusmodifikator der animierten Ansicht. Aktiv ist der Zustand des Beginns der Animation für das Erscheinen der Ansicht oder das Ende der Animation für das Verschwinden, d. H. das andere Ende des Segments, die Zustände, innerhalb derer interpoliert wird.

SpinIn impliziert also, dass ich es verwenden werde, um die Ansicht von außerhalb des Bildschirms (oder des für die Ansicht zugewiesenen Bereichs) anzuzeigen, indem ich sie im Uhrzeigersinn um den angegebenen Punkt drehe. spinOut bedeutet, dass die Ansicht auf dieselbe Weise verschwindet und sich um denselben Punkt dreht, auch im Uhrzeigersinn.

Wenn Sie nach meiner Idee denselben Punkt für das Erscheinen und Verschwinden der Ansicht verwenden, wird der gesamte Bildschirm um diesen Punkt gedreht.

Alle Animationen basieren auf der Standardmodifikatormechanik. Wenn Sie einen vollständig benutzerdefinierten Modifikator schreiben, müssen Sie die Anforderungen des AnimatableModifier-Protokolls implementieren, wie wir es zuvor mit TwoParameterBorder getan haben, oder die darin enthaltenen integrierten Modifikatoren verwenden, die ihre eigene Standardanimation bereitstellen. In diesem Fall habe ich mich auf die integrierte Animation .rotationEffect () in meinem Modifikator SpinTransitionModifier verlassen.

Der Modifikator .transition () verdeutlicht nur, was als Start- und Endpunkt der Animation zu betrachten ist. Wenn Sie den AnimatableData-Status vor dem Starten der Animation anfordern müssen, um den AnimatableData-Modifikator des aktuellen Status anzufordern, die Differenz zu berechnen und dann die Abnahme von 1 auf 0 zu animieren, ändert .transition () nur die Originaldaten. Sie sind nicht an den Status Ihrer Ansicht gebunden, sondern basieren nicht darauf. Sie geben den Anfangs- und Endzustand explizit selbst an, von diesen erhalten Sie AnimatableData, berechnen die Differenz und animieren sie. Am Ende der Animation tritt dann Ihre aktuelle Ansicht in den Vordergrund.

Identität ist übrigens ein Modifikator, der am Ende der Animation auf Ihre Ansicht angewendet wird. Andernfalls würde ein Fehler hier zu Sprüngen am Ende der Erscheinungsanimation und am Anfang der Verschwindenanimation führen. Der Übergang kann also als „zwei in einem“ betrachtet werden. Dabei wird ein bestimmter Modifikator direkt auf Ansicht + angewendet, um die Änderungen zu animieren, wenn die Ansicht angezeigt und ausgeblendet wird.

Ehrlich gesagt scheint mir dieser Animationssteuerungsmechanismus sehr stark zu sein, und es tut mir ein wenig leid, dass wir ihn für keine Animation verwenden können. Ich würde dies nicht ablehnen, um endlose geschlossene Animationen zu erstellen. Wir werden jedoch im nächsten Artikel darüber sprechen.

Um besser zu sehen, wie die Änderung selbst erfolgt, habe ich unsere Testansicht durch elementare Quadrate ersetzt, die mit Zahlen signiert und gerahmt sind.

                    Group{
                        if ind == self.currentViewInd{
                            //self.views[ind]
                            Rectangle()
                                .fill(Color.gray)
                                .frame(width: 100, height: 100)
                                .border(Color.black, width: 2)
                                .overlay(Text("\(ind + 1)"))
                              .transition(.asymmetric(
                                  insertion: .spinIn(anchor: .bottomTrailing),
                                  removal: .spinOut(anchor: .bottomTrailing)))
                        }
                    }


Und um diese Bewegung noch besser zu machen, habe ich .clipped () aus dem SpinTransitionModifier-Modifikator entfernt:

struct SpinTransitionModifier: ViewModifier {
    let angle: Double
    let anchor: UnitPoint
    func body(content: Content) -> some View {
        content
            .rotationEffect(Angle(degrees: angle), anchor: anchor)
            //.clipped()
    }
}


Übrigens brauchen wir jetzt SpinTransitionModifier in unserem eigenen Modifikator. Es wurde nur erstellt, um die beiden Modifikatoren RotationEffect und Clipped () zu einem zu kombinieren, damit die Rotationsanimation nicht über den für unsere Ansicht ausgewählten Bereich hinausgeht. Jetzt können wir .rotationEffect () direkt in .modifier () verwenden. Wir benötigen keinen Vermittler in Form von SpinTransitionModifier.

Wenn die Ansicht stirbt


Ein interessanter Punkt ist der View-Lebenszyklus, wenn er in einem if-else platziert wird. Die Ansicht wird zwar initiiert und als Array-Element aufgezeichnet, jedoch nicht im Speicher gespeichert. All ihreZustandDie Parameter werden beim nächsten Erscheinen auf dem Bildschirm auf die Standardeinstellungen zurückgesetzt. Dies entspricht fast der Initialisierung. Trotz der Tatsache, dass die Objektstruktur selbst noch existiert, hat der Render sie aus ihrem Sichtfeld entfernt, für sie ist es nicht. Dies reduziert zum einen die Speichernutzung. Wenn das Array eine große Anzahl komplexer Ansichten enthält, muss das Rendering diese ständig zeichnen und auf Änderungen reagieren. Dies wirkt sich negativ auf die Leistung aus. Wenn ich mich nicht irre, war dies vor dem Xcode 11.3-Update der Fall. Inaktive Ansichten werden jetzt aus dem Renderspeicher entladen.

Auf der anderen Seite müssen wir alle wichtigen Zustände über den Rahmen dieser Ansicht hinaus verschieben. Verwenden Sie dazu am besten @EnvironmentObject-Variablen.

Zurück zum Lebenszyklus sollte auch beachtet werden, dass der Modifikator .onAppear {}, falls einer in dieser Ansicht registriert ist, unmittelbar nach dem Ändern des Zustands und des Erscheinungsbilds der Ansicht auf dem Bildschirm funktioniert, noch bevor die Animation beginnt. Dementsprechend wird onDisappear {} nach dem Ende der Verschwinden-Animation ausgelöst. Denken Sie daran, wenn Sie sie mit Übergangsanimationen verwenden möchten.

Was weiter?


Puh Es stellte sich ziemlich umfangreich heraus, aber im Detail und, wie ich hoffe, verständlich. Ehrlich gesagt hatte ich gehofft, als Teil eines Artikels über Regenbogenanimation sprechen zu können, aber ich konnte nicht rechtzeitig mit den Details aufhören. Warten Sie also auf die Fortsetzung.

Der folgende Teil erwartet uns:

  • Verwendung von Verläufen: linear, kreisförmig und eckig - alles wird nützlich sein
  • Farbe ist überhaupt keine Farbe: Wählen Sie mit Bedacht aus.
  • Loop-Animation: Wie man startet und wie man stoppt und wie man sofort stoppt (ohne Animation, Ändern der Animation - ja, es gibt auch eine)
  • Aktuelle Stream-Animation: Prioritäten, Überschreibungen, unterschiedliche Animationen für unterschiedliche Objekte
  • Details zu Animations-Timings: Wir werden Timings sowohl im Schwanz als auch in der Mähne fahren, bis hin zu unserer eigenen Implementierung von TimingCurve (oh, halte mich sieben :))
  • wie man den aktuellen Moment der abgespielten Animation herausfindet
  • Wenn SwiftUI nicht ausreicht

Ich werde dies alles am Beispiel der Erstellung einer Regenbogenanimation wie im Bild ausführlich besprechen:


Ich ging nicht den einfachen Weg, sondern sammelte alle Rechen, die ich erreichen konnte, und verkörperte diese Animation nach den oben beschriebenen Prinzipien. Die Geschichte darüber sollte sich als sehr informativ herausstellen und reich an Tricks und allerlei Hacks sein, über die es nur wenige Berichte gab und die für diejenigen nützlich sein werden, die sich entscheiden, ein Pionier in SwiftUI zu werden. Es wird ungefähr in ein oder zwei Wochen erscheinen. Übrigens können Sie abonnieren, um nicht zu verpassen. Dies jedoch natürlich nur, wenn Ihnen das Material nützlich erscheint und die Präsentationsmethode genehmigt ist. Dann hilft Ihr Abonnement dabei, neue Artikel schnell an die Spitze zu bringen und sie frühzeitig einem breiteren Publikum zugänglich zu machen. Andernfalls schreiben Sie in die Kommentare, was falsch ist.

All Articles