Plongez plus profondément dans le Motion Design : transitions iOS avancées

Publié: 2020-11-09

La créativité s'accompagne d'un désir constant d'impressionner l'utilisateur. Pendant des siècles, l'homme a essayé de manipuler les moyens disponibles pour recréer l'interaction la plus naturelle, en prenant la nature comme exemple fondamental.

En découvrant le monde, une personne devient de plus en plus sensible aux détails subtils du monde qui l'entoure, ce qui lui permet de distinguer instinctivement l'artificialité des êtres vivants. Cette ligne s'estompe avec le développement de la technologie, où le logiciel vise à créer un environnement dans lequel son utilisateur décrit comme naturel son expérience dans un monde créé artificiellement.

Faire écho à la nature dans la conception de l'application

Cet article présentera le processus de floutage de la bordure en utilisant l'exemple de la transformation de forme dans l'animation interactive d'éléments quotidiens dans la plupart des applications iOS. Une façon d'imiter la nature consiste à effectuer diverses transformations de la position d'un objet dans le temps. Des exemples de fonctions de temps d'animation sont présentés ci-dessous.

Combiné avec la bonne utilisation du temps en ajoutant une transformation géométrique, nous pouvons obtenir un nombre infini d'effets . Comme démonstration des possibilités de la conception et de la technologie d'aujourd'hui, l'application Motion Patterns a été créée, qui comprend des solutions populaires, développées par notre société de développement de logiciels. Comme je ne suis pas un écrivain, mais un programmeur, et que rien ne parle mieux que des exemples vivants, je n'ai d'autre choix que de vous inviter dans ce monde merveilleux !

Des échantillons à découvrir

Voyons comment le motion design peut transformer un design banal en quelque chose d'exceptionnel ! Dans les exemples ci-dessous, sur le côté gauche, il y a une application qui utilise uniquement des animations iOS de base, tandis que sur le côté droit, il y a une version de la même application avec quelques améliorations.

Effet "Dive Deeper"

Il s'agit d'une transition utilisant la transformation entre deux états d'une vue . Construit sur la base d'une collection, après avoir sélectionné une cellule spécifique, la transition vers les détails d'un élément se fait en transformant ses éléments individuels *. Une solution supplémentaire est l'utilisation de transitions interactives, qui facilitent l'utilisation de l'application.

*en fait copier/mapper des éléments de données sur une vue temporaire participant à la transition, entre son début et sa fin… mais je vous expliquerai cela plus loin dans cet article…

Effet "Peek Over the Edge"

L'utilisation de l'animation scroll view dans son action transforme l'image sous forme de 3D pour un effet cube . Le principal facteur responsable de l'effet est le décalage de la vue de défilement.

Effet "Relier les points"

Il s'agit d'une transition entre les scènes qui transforme l'objet miniature en écran entier . Les collections utilisées à cet effet fonctionnent en parallèle, un changement sur un écran correspond à un décalage sur l'autre. De plus, lorsque vous entrez dans la miniature en arrière-plan, un effet de parallaxe apparaît lorsque vous glissez entre les scènes.

Effet "Déplacer la forme"

Le dernier type d'animation est simple et utilise la bibliothèque Lottie. C'est l'utilisation la plus courante pour animer des icônes. Dans ce cas, ce sont les icônes de la barre d'onglets. De plus, en changeant les onglets appropriés, une animation de la transition dans une direction spécifique a été utilisée pour intensifier davantage l'effet d'interaction.

Plongez plus profondément : notre premier modèle de motion design

Il est maintenant temps d'en venir au fait… nous devons approfondir encore la structure des mécanismes qui contrôlent ces exemples.

Dans cet article, je vais vous présenter le premier motion design pattern , que nous avons nommé 'Dive Deeper' avec une description abstraite de son utilisation, sans rentrer dans les détails spécifiques. Nous prévoyons de rendre le code exact et l'intégralité du référentiel accessibles à tous à l'avenir, sans aucune restriction.

L'architecture du projet et les modèles de conception de programmation stricts appliqués ne sont pas une priorité pour le moment - nous nous concentrons sur les animations et la transition.

Dans cet article, nous utiliserons deux ensembles de fonctionnalités fournies pour gérer les vues lors des transitions de scène. Ainsi, je tiens à préciser que cet article est destiné aux personnes relativement familiarisées avec UIKit et la syntaxe Swift.

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

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

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

Premièrement : la structure

Pour la version de base implémentant une solution donnée, plusieurs classes d'assistance seront nécessaires, chargées de fournir les informations nécessaires sur les vues impliquées dans la transition, et de contrôler la transition elle-même et les interactions.

Transition en Swift

La classe de base chargée de gérer la transition, le proxy, sera TransitionAnimation. Il décide de la manière dont la transition aura lieu et couvre les fonctions standard nécessaires pour effectuer l'action fournie par l'équipe Apple.

 /// Il s'agit d'une classe fondamentale pour les transitions qui ont un comportement différent lors de la présentation et du rejet sur une durée spécifiée.
classe ouverte TransitionAnimator : NSObject, UIViewControllerAnimatedTransitioning {
    
    /// Indique s'il s'agit de présenter ou de rejeter une transition.
    var présentant : Bool = true
    
    /// Intervalle de temps pendant lequel toute la transition a lieu.
    durée de la location privée : TimeInterval
    
    /// Initialisateur par défaut de l'animateur de transition avec les valeurs par défaut.
    /// - Durée du paramètre : Intervalle de temps pendant lequel la transition entière a lieu.
    /// - Paramètre présentant : indicateur s'il présente ou rejette une transition.
    public init(duration : TimeInterval = 0.5, présentation : Bool = true) {
        self.duration = durée
        self.presenting = présentation
        super.init()
    }
    
    /// Détermine la durée de la transition.
    /// - Paramètre transitionContext : Contexte de la transition en cours.
    /// - Renvoie : la durée spécifiée à l'initialisation de l'animateur.
    fonction publique transitionDuration (en utilisant transitionContext : UIViewControllerContextTransitioning ?) -> TimeInterval {
        retour self.duration
    }
    
    /// Cœur de l'animateur de transition, dans cette fonction la transition a lieu.
    /// - Important : Remplacer cette fonction dans un type de transition plus concret est crucial pour réaliser des animations.
    /// - Paramètre transitionContext : Contexte de la transition.
    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { }
    
}

Sur la base de TransitionAnimator, nous créons un fichier TransformTransition, dont la tâche sera d'effectuer une transition spécifique, transition avec transformation (parentage)

 /// Implémentation de la transition transform.
classe ouverte TransformTransition : TransitionAnimator {
    
    /// Afficher le modèle contenant toutes les spécifications de transition nécessaires.
    var privé viewModel : TransformViewModel
    
    /// Initialiseur par défaut de la transition de transformation.
    /// - Paramètre viewModel : modèle de vue de la transition de transformation.
    /// - Durée du paramètre : durée de la transition.
    init(viewModel : TransformViewModel, durée : TimeInterval) {
        self.viewModel = viewModel
        super.init(durée : durée, présentation : viewModel.presenting)
    }

La composition de la classe TransformTransition inclut le TransformViewModel, qui, comme son nom l'indique, informe le mécanisme des modèles de vue auxquels cette transition s'appliquera.

 /// Afficher le modèle de transition de transformation qui contient des informations de base à son sujet.
classe finale TransformViewModel {
    
    /// Indique si la transition de transformation présente ou supprime la vue.
    laissez présenter: Bool
    /// Tableau de modèles avec spécification de transformation pour chaque vue.
    laissez les modèles : [TransformModel]
    
    /// Initialiseur par défaut du modèle de vue de transformation.
    /// - Présentation du paramètre : indique s'il présente ou s'il rejette la transition de transformation.
    /// - Modèles de paramètres : tableau de modèles avec spécification de transformation pour chaque vue.
    init(présentant : Bool, modèles : [TransformModel]) {
        self.presenting = présentation
        self.models = modèles
    }
    
}

Le modèle de transformation est une classe auxiliaire qui décrit les éléments spécifiques des vues impliquées dans la transition située dans le parent, généralement les vues d'un contrôleur qui peuvent être transformées.

Dans le cas d'une transition, c'est une étape nécessaire car cette transition consiste en des opérations de vues spécifiques entre des états donnés.

Deuxièmement : la mise en œuvre

Nous étendons le modèle de vue à partir duquel nous commençons la transition avec Transformable, ce qui nous oblige à implémenter une fonction qui préparera tous les éléments nécessaires. La taille de cette fonction peut croître très rapidement, je vous suggère donc de la décomposer en parties plus petites, par exemple par élément.

 /// Protocole pour la classe qui souhaite effectuer une transition de transformation.
protocole Transformable : ViewModel {
    
    /// Prépare des modèles de vues impliquées dans la transition.
    /// - Paramètre fromView : La vue à partir de laquelle la transition commence
    /// - Paramètre toView : La vue vers laquelle la transition va.
    /// - Paramètre présentant : indique s'il s'agit d'une présentation ou d'un rejet.
    /// - Renvoie : un tableau de structures contenant toutes les informations nécessaires prêtes à transformer la transition pour chaque vue.
    func prepareTransitionModels(fromView : UIView, toView : UIView, présentant : Bool) -> [TransformModel]
    
}

L'hypothèse n'est pas de dire comment rechercher les données des vues participant à la transformation. Dans mon exemple, j'ai utilisé des balises qui représentent une vue donnée. Vous avez les mains libres dans cette partie de la mise en œuvre.

Les modèles de transformations de vue spécifiques (TransformModel) sont le plus petit modèle de toute la liste. Ils se composent d'informations de transformation clés telles que la vue de début, la vue de transition, l'image de début, l'image de fin, le centre de début, le centre de fin, les animations simultanées et l'opération de fin. La plupart des paramètres n'ont pas besoin d'être utilisés lors de la transformation, ils ont donc leurs propres valeurs par défaut. Pour des résultats minimes, il suffit de n'utiliser que ceux qui sont nécessaires.

 /// Initialiseur par défaut du modèle de transformation avec les valeurs par défaut.
    /// - Paramètre initialView : Vue à partir de laquelle la transition démarre.
    /// - Paramètre phantomView : vue présentée lors de la transition de transformation.
    /// - Paramètre initialFrame : cadre de la vue qui démarre la transition de transformation.
    /// - Paramètre finalFrame : Cadre de vue qui sera présenté à la fin de la transition de transformation.
    /// - Paramètre initialCenter : nécessaire lorsque le point de vue central initial est différent du centre de la vue initiale.
    /// - Paramètre finalCenter : nécessaire lorsque le point de vue central final est différent du centre de la vue finale.
    /// - Paramètre parallelAnimation : animation supplémentaire de la vue effectuée lors de la transition de transformation.
    /// - Complétion des paramètres : bloc de code déclenché après la transition de transformation.
    /// - Remarque : Seule la vue initiale est nécessaire pour effectuer la version la plus minimaliste de la transition de transformation.
    init (vue initiale : UIView,
         vue fantôme : UIView = UIView(),
         cadre initial : CGRect = CGRect(),
         cadre final : CGRect = CGRect(),
         initialCenter : CGPoint ? = nul,
         Centre final : CGPoint ? = nul,
         parallelAnimation : (() -> Vide) ? = nul,
         achèvement : (() -> Vide) ? = nul) {
        self.initialView = initialView
        self.phantomView = phantomView
        self.initialFrame = initialFrame
        self.finalFrame = finalFrame
        self.parallelAnimation = parallelAnimation
        self.completion = achèvement
        self.initialCenter = initialCenter ?? CGPoint(x : initialFrame.midX, y : initialFrame.midY)
        self.finalCenter = finalCenter ?? CGPoint(x : finalFrame.midX, y : finalFrame.midY)
    }

Votre attention a peut-être été captée par la vue fantôme. C'est le moment où je vais vous expliquer le workflow des transitions iOS. Sous la forme la plus courte possible…

Le flux de travail pour les transitions iOS

Lorsque l'utilisateur souhaite passer à la scène suivante, iOS prépare des contrôleurs spécifiques en copiant les contrôleurs de départ (bleu) et cible (vert) dans la mémoire. Ensuite, un contexte de transition est créé via le coordinateur de transition qui contient le conteneur, une vue "stupide" qui ne contient aucune fonction spéciale, en plus de simuler les vues de transition entre les deux scènes.

Le principe clé de l'utilisation des transitions est de ne pas ajouter de vue réelle au contexte de transition, car à la fin de la transition, tout le contexte est désalloué, ainsi que les vues ajoutées au conteneur. Ce sont des vues qui n'existent que pendant la transition et qui sont ensuite supprimées.

Par conséquent, l'utilisation de vues fantômes qui sont des répliques de vues réelles est une solution importante à cette transition.

Dans ce cas, nous avons une transition qui transforme une vue en une autre en changeant sa forme et sa taille. Pour ce faire, au début de la transition, je crée un PhantomView de l'élément donné et l'ajoute au conteneur. FadeView est une vue auxiliaire pour ajouter de la douceur à la transition globale.

 /// Cœur de la transition de transformation, où la transition s'exécute. Remplacement de `TransitionAnimator.animateTransition(...)`.
    /// - Paramètre transitionContext : Le contexte de la transition de transformation actuelle.
    remplacer open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let toViewController = transitionContext.view(forKey: .to),
            laissez fromViewController = transitionContext.view(forKey: .from) else {
                retourner Log.unexpectedState()
        }
        laissez containerView = transitionContext.containerView
        laisser durée = transitionDuration (en utilisant : transitionContext)
        laissez fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue)
        let models = viewModel.models
        laissez presentedView = présentation ? toViewController : fromViewController
        
        modèles.forEach { $0.initialView.isHidden = true }
        presentedView.isHidden = vrai
        conteneurView.addSubview(toViewController)
        si présentant {
            containerView.insertSubview(fadeView, belowSubview : toViewController)
        } autre {
            conteneurView.addSubview(fadeView)
        }
        conteneurView.addSubviews(viewModel.models.map { $0.phantomView })

Dans l'étape suivante, je le transforme en forme cible par des transformations, et selon qu'il s'agit d'une présentation ou d'un rappel, il effectue des opérations supplémentaires pour nettoyer des vues spécifiques - c'est toute la recette de cette transition.

 let animations : () -> Void = { [weak self] in
            guard let self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            modèles.forEach {
                let center = self.presenting ? $0.finalCenter : $0.initialCenter
                let transform = self.presenting ? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(transformer, centrer)
            }
            modèles.compactMap { $0.parallelAnimation }.forEach { $0() }
        }
        
        let complétion : (Bool) -> Void = { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            presentedView.isHidden = faux
            modèles.compactMap { $0.completion }.forEach { $0() }
            modèles.forEach { $0.initialView.isHidden = false }
            if !self.presenting && transitionContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration : durée,
                       retard : 0,
                       usingSpringWithDamping: 1,
                       vitesse initiale du ressort : 0,5,
                       options : .curveEaseOut,
                       animations : animations,
                       achèvement : achèvement)

Troisièmement : l'ingrédient spécial

Après avoir rassemblé toutes les fonctions, classes et protocoles, le résultat devrait ressembler à ceci :

La dernière composante de notre transition sera sa pleine interactivité. Pour cela, nous utiliserons un Pan Gesture ajouté dans la vue du contrôleur, TransitionInteractor…

 /// Médiateur pour gérer la transition interactive.
classe finale TransitionInteractor : UIPercentDrivenInteractiveTransition {
    
    /// Indique si la transition a commencé.
    var a commencé = faux
    /// Indique si la transition doit se terminer.
    var devraitFinir = faux

}

… que nous initialisons également dans le corps du contrôleur.

 /// Gère le geste de panoramique sur les éléments de la vue de collection et gère la transition.
    @objc func handlePanGesture(_gesteRecognizer : UIPanGestureRecognizer) {
        laissez percentThreshold: CGFloat = 0.1
        let translation =gesteRecognizer.translation(in: view)
        laissez verticalMovement = translation.y / view.bounds.height
        soit vers le hautMovement = fminf(Float(verticalMovement), 0.0)
        soit upwardMovementPercent = fminf(abs(upwardMovement), 0.9)
        laisser progresser = CGFloat (upwardMovementPercent)
        guard let interactor = interactionController else { return }
        changer de reconnaissance de geste.state {
        l'affaire a commencé :
            interacteur.hasStarted = vrai
            laissez tapPosition =gesteRecognizer.location(in: collectionView)
            showDetailViewControllerFrom(location: tapPosition)
        cas .changé :
            interactor.shouldFinish = progress > percentThreshold
            interacteur.mise à jour (progrès)
        affaire .annulée :
            interacteur.hasStarted = faux
            interacteur.annuler()
        affaire .terminée :
            interacteur.hasStarted = faux
            interactor.shouldFinish
                ? interacteur.finir()
                : interacteur.annuler()
        défaut:
            Pause
        }
    }

L'interaction prête devrait être la suivante :

Si tout se passe comme prévu, notre application gagnera beaucoup plus aux yeux de ses utilisateurs.

Découvert seulement un sommet d'un iceberg

La prochaine fois, j'expliquerai l'implémentation des problématiques liées au motion design dans les prochaines éditions.

L'application, le design et le code source sont la propriété de Miquido, et ont été créés avec passion par des designers et programmeurs talentueux dont nous ne sommes pas responsables dans nos implémentations. Le code source détaillé sera disponible à l'avenir via notre compte github — nous vous invitons à nous suivre !

Merci de votre attention et à bientôt !