Tauchen Sie tiefer in Motion Design ein: Erweiterte iOS-Übergänge

Veröffentlicht: 2020-11-09

Kreativität bringt den ständigen Wunsch mit sich, den Benutzer zu beeindrucken. Seit Jahrhunderten versucht der Mensch, die verfügbaren Mittel zu manipulieren, um die natürlichste Interaktion nachzubilden, wobei er sich die Natur als grundlegendes Beispiel nimmt.

Bei der Entdeckung der Welt wird eine Person immer sensibler für die subtilen Details der Welt um sie herum, was es ihnen ermöglicht, Künstlichkeit von Lebewesen instinktiv zu unterscheiden. Diese Grenze verschwimmt mit der Entwicklung der Technologie, wo die Software darauf abzielt, eine Umgebung zu schaffen, in der ihr Benutzer seine Erfahrung in einer künstlich geschaffenen Welt als natürlich beschreibt.

Widerhall der Natur im Design der App

Dieser Artikel stellt den Prozess des Verwischens der Grenze am Beispiel der Formtransformation in der interaktiven Animation alltäglicher Elemente in den meisten iOS-Anwendungen vor. Eine Möglichkeit, die Natur nachzuahmen, besteht in verschiedenen Transformationen der Position eines Objekts in der Zeit. Beispielhafte Funktionen der Animationszeit sind unten dargestellt.

In Kombination mit der richtigen Nutzung der Zeit durch Hinzufügen einer geometrischen Transformation können wir eine unendliche Anzahl von Effekten erzielen . Als Demonstration der Möglichkeiten des heutigen Designs und der heutigen Technologie wurde die Motion Patterns-Anwendung erstellt, die beliebte Lösungen enthält, die von unserer Softwareentwicklungsfirma entwickelt wurden. Da ich kein Schriftsteller, sondern Programmierer bin und nichts besser spricht als lebendige Beispiele, habe ich keine andere Wahl, als Sie in diese wunderbare Welt einzuladen!

Proben zum Entdecken

Werfen wir einen Blick darauf, wie Motion Design ein 08/15-Design in etwas Außergewöhnliches verwandeln kann! In den folgenden Beispielen befindet sich auf der linken Seite eine Anwendung, die nur grundlegende iOS-Animationen verwendet, während auf der rechten Seite eine Version derselben Anwendung mit einigen Verbesserungen zu sehen ist.

„Dive Deeper“-Effekt

Dies ist ein Übergang, der eine Transformation zwischen zwei Zuständen einer Ansicht verwendet . Aufbauend auf einer Sammlung erfolgt nach Auswahl einer bestimmten Zelle der Übergang zu den Details eines Elements durch Transformation seiner einzelnen Elemente *. Eine weitere Lösung ist die Verwendung von interaktiven Übergängen, die die Nutzung der Anwendung erleichtern.

*Eigentliches Kopieren/Mapping von Datenelementen auf eine temporäre Ansicht, die am Übergang teilnimmt, zwischen ihrem Anfang und ihrem Ende … aber ich werde dies später in diesem Artikel erklären …

„Spähen Sie über den Rand“-Effekt

Die Verwendung der Scroll-View-Animation in ihrer Aktion transformiert das Bild in 3D-Form für einen Würfeleffekt . Der Hauptfaktor, der für den Effekt verantwortlich ist, ist der Versatz der Bildlaufansicht.

„Verbinde die Punkte“-Effekt

Dies ist ein Übergang zwischen Szenen, der das Miniaturobjekt in den gesamten Bildschirm verwandelt . Die dafür verwendeten Sammlungen arbeiten parallel, eine Änderung auf einem Bildschirm entspricht einer Verschiebung auf dem anderen. Wenn Sie die Miniatur im Hintergrund betreten, erscheint außerdem ein Parallaxeneffekt, wenn Sie zwischen den Szenen wischen.

Effekt „Form verschieben“.

Die letzte Art der Animation ist eine einfache, die die Lottie-Bibliothek verwendet. Dies ist die häufigste Verwendung zum Animieren von Symbolen. In diesem Fall sind dies die Symbole in der Registerkartenleiste. Zusätzlich wurde durch Wechseln der entsprechenden Tabs eine Animation des Übergangs in eine bestimmte Richtung eingesetzt, um den Interaktionseffekt weiter zu intensivieren.

Tauchen Sie tiefer ein: unser erstes Motion-Design-Pattern

Jetzt ist es an der Zeit, auf den Punkt zu kommen … wir müssen noch tiefer in die Struktur der Mechanismen vordringen, die diese Beispiele steuern.

In diesem Artikel stelle ich Ihnen das erste Motion Design Pattern vor, das wir „Dive Deeper“ genannt haben, mit einer abstrakten Beschreibung seiner Verwendung, ohne auf spezifische Details einzugehen. Wir planen, den genauen Code und das gesamte Repository in Zukunft ohne Einschränkungen für alle verfügbar zu machen.

Die Architektur des Projekts und angewandte strenge Programmierdesignmuster haben derzeit keine Priorität – wir konzentrieren uns auf Animationen und Übergänge.

In diesem Artikel verwenden wir zwei Funktionssätze, die zum Verwalten von Ansichten während Szenenübergängen bereitgestellt werden. Daher möchte ich darauf hinweisen, dass dieser Artikel für Personen gedacht ist, die mit UIKit und der Swift-Syntax relativ vertraut sind.

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html

Erstens: die Struktur

Für die Basisversion, die eine bestimmte Lösung implementiert, werden mehrere Hilfsklassen benötigt, die dafür verantwortlich sind, die notwendigen Informationen über die am Übergang beteiligten Ansichten bereitzustellen und den Übergang selbst und die Interaktionen zu steuern.

Übergang in Swift

Die grundlegende Klasse, die für die Verwaltung des Übergangs verantwortlich ist, der Proxy, ist TransitionAnimation. Es entscheidet, auf welche Weise der Übergang erfolgt und deckt die Standardfunktionen ab, die zum Ausführen der vom Apple-Team bereitgestellten Aktion erforderlich sind.

 /// Dies ist eine grundlegende Klasse für Übergänge, die ein unterschiedliches Verhalten beim Präsentieren und Verwerfen über eine bestimmte Dauer haben.
offene Klasse TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// Zeigt an, ob es einen Übergang präsentiert oder verwirft.
    var präsentiert: Bool = true
    
    /// Zeitintervall, in dem der gesamte Übergang stattfindet.
    Privatmietdauer: TimeInterval
    
    /// Standardinitialisierer des Übergangsanimators mit Standardwerten.
    /// - Parameter Dauer: Zeitintervall, in dem der gesamte Übergang stattfindet.
    /// - Parameterpräsentation: Indikator, ob es einen Übergang präsentiert oder verwirft.
    public init(Dauer: TimeInterval = 0.5, Präsentieren: Bool = true) {
        self.duration = Dauer
        self.presenting = präsentieren
        super.init()
    }
    
    /// Bestimmt die Dauer des Übergangs.
    /// - Parameter transitContext: Kontext des aktuellen Übergangs.
    /// - Rückgabe: Angegebene Dauer bei der Initialisierung des Animators.
    öffentliche Funktion transitDuration (mit transitContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        Rückkehr selbst.Dauer
    }
    
    /// Herz des Übergangsanimators, in dieser Funktion findet der Übergang statt.
    /// - Wichtig: Das Überschreiben dieser Funktion in einem konkreteren Übergangstyp ist entscheidend für die Ausführung von Animationen.
    /// - Parameter transitContext: Kontext des Übergangs.
    öffentliche Funktion animateTransition (unter Verwendung von transitContext: UIViewControllerContextTransitioning) { }
    
}

Basierend auf dem TransitionAnimator erstellen wir eine TransformTransition-Datei, deren Aufgabe darin besteht, einen bestimmten Übergang durchzuführen, Übergang mit Transformation (Elternschaft).

 /// Implementierung des Transformationsübergangs.
offene Klasse TransformTransition: TransitionAnimator {
    
    /// Modell anzeigen, das alle erforderlichen Übergangsspezifikationen enthält.
    private var viewModel: TransformViewModel
    
    /// Standard-Initialisator des Transformationsübergangs.
    /// - Parameter viewModel: Ansichtsmodell des Transformationsübergangs.
    /// - Parameter duration: Dauer des Übergangs.
    init(viewModel: TransformViewModel, Dauer: TimeInterval) {
        self.viewModel = Ansichtsmodell
        super.init (Dauer: Dauer, Präsentieren: viewModel.presenting)
    }

Die Zusammensetzung der TransformTransition-Klasse enthält das TransformViewModel, das, wie der Name schon sagt, den Mechanismus darüber informiert, auf welche Ansichtsmodelle dieser Übergang angewendet wird.

 /// Modell des Transformationsübergangs anzeigen, das grundlegende Informationen darüber enthält.
letzte Klasse TransformViewModel {
    
    /// Gibt an, ob der Transformationsübergang die Ansicht präsentiert oder verwirft.
    let Präsentieren: Bool
    /// Array von Modellen mit Angabe zur Transformation für jede Ansicht.
    Lassen Sie Modelle: [TransformModel]
    
    /// Standardinitialisierer des Transformationsansichtsmodells.
    /// - Parameterpräsentation: Gibt an, ob der Transformationsübergang präsentiert oder verworfen wird.
    /// - Parametermodelle: Array von Modellen mit Angaben zur Transformation für jede Ansicht.
    init (Präsentieren: Bool, Modelle: [TransformModel]) {
        self.presenting = präsentieren
        self.models = Modelle
    }
    
}

Das Transformationsmodell ist eine Hilfsklasse, die die spezifischen Elemente der Ansichten beschreibt, die an dem Übergang beteiligt sind, der sich in der übergeordneten Klasse befindet, normalerweise die Ansichten eines Controllers, die transformiert werden können.

Im Fall eines Übergangs ist dies ein notwendiger Schritt, da dieser Übergang aus den Operationen bestimmter Ansichten zwischen gegebenen Zuständen besteht.

Zweitens: die Umsetzung

Wir erweitern das Ansichtsmodell, von dem aus wir den Übergang mit Transformable beginnen, was uns zwingt, eine Funktion zu implementieren, die alle erforderlichen Elemente vorbereitet. Die Größe dieser Funktion kann sehr schnell wachsen, daher schlage ich vor, dass Sie sie in kleinere Teile zerlegen, zum Beispiel pro Element.

 /// Protokoll für die Klasse, die den Transformationsübergang durchführen möchte.
Protokoll Transformierbar: ViewModel {
    
    /// Bereitet Modelle von Views vor, die am Übergang beteiligt sind.
    /// - Parameter fromView: Die Ansicht, von der aus der Übergang beginnt
    /// - Parameter toView: Die Ansicht, zu der der Übergang geht.
    /// - Parameterpräsentation: Gibt an, ob präsentiert oder verworfen wird.
    /// - Rückgaben: Array von Strukturen, das alle erforderlichen Informationen bereithält, um den Übergang für jede Ansicht zu transformieren.
    func PrepareTransitionModels(fromView: UIView, toView: UIView, Presenting: Bool) -> [TransformModel]
    
}

Die Annahme besteht nicht darin, zu sagen, wie nach den Daten der Ansichten gesucht werden soll, die an der Transformation teilnehmen. In meinem Beispiel habe ich Tags verwendet, die eine bestimmte Ansicht darstellen. In diesem Teil der Umsetzung haben Sie freie Hand.

Die Modelle bestimmter Ansichtstransformationen (TransformModel) sind das kleinste Modell in der gesamten Liste. Sie bestehen aus wichtigen Transformationsinformationen wie Startansicht, Übergangsansicht, Startframe, Endframe, Startzentrum, Endzentrum, gleichzeitige Animationen und Endoperation. Die meisten Parameter müssen während der Transformation nicht verwendet werden, daher haben sie ihre eigenen Standardwerte. Für minimale Ergebnisse reicht es aus, nur die erforderlichen zu verwenden.

 /// Standardinitialisierer des Transformationsmodells mit Standardwerten.
    /// - Parameter initialView: Ansicht, von der aus der Übergang beginnt.
    /// - Parameter phantomView: Ansicht, die während des Transformationsübergangs präsentiert wird.
    /// - Parameter initialFrame: Sichtrahmen, der den Transformationsübergang startet.
    /// - Parameter finalFrame: Ansichtsrahmen, der am Ende des Transformationsübergangs präsentiert wird.
    /// - Parameter initialCenter: Wird benötigt, wenn der Blickpunkt des Anfangszentrums anders ist als der Mittelpunkt der Anfangsansicht.
    /// - Parameter finalCenter: Benötigt, wenn der Blickpunkt des endgültigen Mittelpunkts anders ist als der Mittelpunkt der endgültigen Ansicht.
    /// - Parameter parallelAnimation: Zusätzliche Animation der Ansicht während des Transformationsübergangs.
    /// - Parametervervollständigung: Codeblock, der nach dem Transformationsübergang ausgelöst wird.
    /// - Hinweis: Nur die anfängliche Ansicht wird benötigt, um die minimalistischste Version des Transformationsübergangs durchzuführen.
    init(initialView: UIView,
         phantomView: UIView = UIView(),
         initialFrame: CGRect = CGRect(),
         finalFrame: CGRect = CGRect(),
         initialCenter: CGPunkt? = null,
         finalCenter: CGPoint? = null,
         parallelAnimation: (() -> Void)? = null,
         Vervollständigung: (() -> Void)? = null) {
        self.initialView = Anfangsansicht
        self.phantomView = phantomView
        self.initialFrame = initialFrame
        self.finalFrame = finalFrame
        self.parallelAnimation = parallelAnimation
        self.completion = Vollendung
        self.initialCenter = initialCenter ?? CGPoint(x: initialFrame.midX, y: initialFrame.midY)
        self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    }

Möglicherweise wurde Ihre Aufmerksamkeit von der Phantomansicht erregt. Dies ist der Moment, in dem ich den Workflow für iOS-Übergänge erkläre. In kürzester Form …

Der Workflow für iOS-Übergänge

Wenn der Benutzer zur nächsten Szene wechseln möchte, bereitet iOS bestimmte Controller vor, indem es die Start- (blau) und Ziel-Controller (grün) in den Speicher kopiert. Als nächstes wird durch den Übergangskoordinator ein Übergangskontext erstellt, der den Container enthält, eine "dumme" Ansicht, die keine spezielle Funktion enthält, außer der Simulation der Übergangsansichten zwischen den beiden Szenen.

Das Schlüsselprinzip beim Arbeiten mit Übergängen besteht darin, dem Übergangskontext keine echte Ansicht hinzuzufügen, da am Ende des Übergangs der gesamte Kontext zusammen mit den dem Container hinzugefügten Ansichten aufgehoben wird. Das sind Ansichten, die nur während des Übergangs existieren und dann entfernt werden.

Daher ist die Verwendung von Phantomansichten, die Nachbildungen echter Ansichten sind, eine wichtige Lösung für diesen Übergang.

In diesem Fall haben wir einen Übergang, der eine Ansicht in eine andere umwandelt, indem er seine Form und Größe ändert. Dazu erstelle ich zu Beginn des Übergangs eine PhantomView des angegebenen Elements und füge sie dem Container hinzu. FadeView ist eine Hilfsansicht, um dem Gesamtübergang Weichheit zu verleihen.

 /// Herzstück des Transformationsübergangs, wo der Übergang ausgeführt wird. Überschreiben von `TransitionAnimator.animateTransition(...)`.
    /// - Parameter transitContext: Der Kontext des aktuellen Transformationsübergangs.
    open func animateTransition überschreiben (mit transitContext: UIViewControllerContextTransitioning) {
        guard let toViewController = transitContext.view(forKey: .to),
            let fromViewController = transitContext.view(forKey: .from) else {
                Log.unexpectedState() zurückgeben
        }
        let containerView = transitContext.containerView
        Lassen Sie Dauer = Übergangsdauer (unter Verwendung von: Übergangskontext)
        let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue)
        Lassen Sie Modelle = viewModel.models
        let presentView = Präsentieren ? toViewController : fromViewController
        
        models.forEach { $0.initialView.isHidden = true }
        presentView.isHidden = wahr
        containerView.addSubview(toViewController)
        wenn präsentiert {
            containerView.insertSubview(fadeView, belowSubview: toViewController)
        } anders {
            containerView.addSubview(fadeView)
        }
        containerView.addSubviews(viewModel.models.map { $0.phantomView })

Im nächsten Schritt verwandle ich es durch Transformationen in die Zielform und je nachdem, ob es sich um eine Präsentation oder einen Rückruf handelt, führt es zusätzliche Operationen aus, um bestimmte Ansichten zu bereinigen – das ist das gesamte Rezept für diesen Übergang.

 Animationen lassen: () -> Void = { [schwaches Selbst] in
            guard let self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            models.forEach {
                let center = self.presenting ? $0.finalCenter : $0.initialCenter
                let transform = self.presenting ? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(transformieren, zentrieren)
            }
            models.compactMap { $0.parallelAnimation }.forEach { $0() }
        }
        
        Let Vervollständigung: (Bool) -> Void = { _ in
            transitContext.completeTransition(!transitionContext.transitionWasCancelled)
            presentView.isHidden = falsch
            models.compactMap { $0.completion }.forEach { $0() }
            models.forEach { $0.initialView.isHidden = false }
            if !self.presenting && transitContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration: Dauer,
                       Verzögerung: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 0,5,
                       Optionen: .curveEaseOut,
                       Animationen: Animationen,
                       Abschluss: Abschluss)

Drittens: die besondere Zutat

Nach dem Zusammenstellen aller Funktionen, Klassen und Protokolle sollte das Ergebnis so aussehen:

Die letzte Komponente unseres Übergangs wird seine vollständige Interaktivität sein. Zu diesem Zweck verwenden wir eine in der Controller-Ansicht hinzugefügte Pan-Geste, TransitionInteractor…

 /// Vermittler, um den interaktiven Übergang zu handhaben.
letzte Klasse TransitionInteractor: UIPercentDrivenInteractiveTransition {
    
    /// Gibt an, ob der Übergang begonnen hat.
    var hasStarted = false
    /// Gibt an, ob der Übergang beendet werden soll.
    var shouldFinish = falsch

}

… die wir auch im Controller-Body initialisieren.

 /// Behandelt die Pan-Geste bei Sammlungsansichtselementen und verwaltet den Übergang.
    @objc func handlePanGesture(_ GesteRecognizer: UIPanGestureRecognizer) {
        Lassen Sie percentThreshold: CGfloat = 0,1
        let translation = GesteRecognizer.translation (in: Ansicht)
        let verticalMovement = translation.y / view.bounds.height
        lass Aufwärtsbewegung = fminf (Float (vertikale Bewegung), 0,0)
        lass AufwärtsbewegungProzent = fminf(abs(Aufwärtsbewegung), 0,9)
        let progress = CGfloat(upwardMovementPercent)
        Guard Let Interactor = InteractionController Else { Return }
        Schalter GesteRecognizer.state {
        Fall .begann:
            interaktor.hasStarted = wahr
            let tapPosition = GesteRecognizer.location(in: collectionView)
            showDetailViewControllerFrom(location: tapPosition)
        Fall .geändert:
            interactiveor.shouldFinish = Fortschritt > PercentThreshold
            interactiver.update (Fortschritt)
        Fall .storniert:
            interactiver.hasStarted = falsch
            interaktor.cancel()
        Fall .beendet:
            interactiver.hasStarted = falsch
            interaktor.sollteFinish
                ? interaktor.finish()
                : interaktor.cancel()
        Ursprünglich:
            Unterbrechung
        }
    }

Die fertige Interaktion sollte wie folgt aussehen:

Wenn alles nach Plan gelaufen ist, wird unsere Anwendung in den Augen ihrer Benutzer viel mehr gewinnen.

Entdeckt nur eine Spitze eines Eisbergs

Das nächste Mal werde ich in den folgenden Ausgaben die Umsetzung verwandter Themen des Motion Designs erläutern.

Die Anwendung, das Design und der Quellcode sind Eigentum von Miquido und wurden mit Leidenschaft von talentierten Designern und Programmierern erstellt, für deren Verwendung wir in unseren Implementierungen nicht verantwortlich sind. Der detaillierte Quellcode wird in Zukunft über unseren Github-Account verfügbar sein – wir laden Sie ein, uns zu folgen!

Vielen Dank für Ihre Aufmerksamkeit und bis bald!