Immergiti nel design del movimento: transizioni iOS avanzate
Pubblicato: 2020-11-09La creatività porta con sé un desiderio costante di impressionare l'utente. Per secoli l'uomo ha cercato di manipolare i mezzi a disposizione per ricreare l'interazione più naturale, prendendo la natura come esempio fondamentale.
Quando scopre il mondo, una persona diventa sempre più sensibile ai sottili dettagli del mondo che la circonda, il che consente loro di distinguere istintivamente l'artificialità dagli esseri viventi. Questa linea è offuscata con lo sviluppo della tecnologia, in cui il software mira a creare un ambiente in cui il suo utente descrive la sua esperienza in un mondo creato artificialmente come naturale.

Riprendendo la natura nel design dell'app
Questo articolo introdurrà il processo di sfocatura del bordo usando l'esempio della trasformazione della forma nell'animazione interattiva di elementi quotidiani nella maggior parte delle applicazioni iOS. Un modo per imitare la natura è attraverso varie trasformazioni della posizione di un oggetto nel tempo. Di seguito sono presentate funzioni esemplificative del tempo di animazione.

Abbinato al giusto uso del tempo aggiungendo una trasformazione geometrica, possiamo ottenere un numero infinito di effetti . A dimostrazione delle possibilità del design e della tecnologia odierni, è stata creata l'applicazione Motion Patterns, che include soluzioni popolari, sviluppate dalla nostra società di sviluppo software. Dato che non sono uno scrittore, ma un programmatore, e niente parla meglio degli esempi dal vivo, non ho altra scelta che invitarti in questo mondo meraviglioso!
Campioni da scoprire

Diamo un'occhiata a come il motion design può trasformare un design comune in qualcosa di eccezionale! Negli esempi seguenti, sul lato sinistro c'è un'applicazione che utilizza solo animazioni iOS di base, mentre sul lato destro c'è una versione della stessa applicazione con alcuni miglioramenti.
Effetto “Immergiti più a fondo”.
Questa è una transizione che utilizza la trasformazione tra due stati di una vista . Costruito sulla base di una collezione, dopo aver selezionato una cella specifica, il passaggio ai dettagli di un elemento avviene trasformandone i singoli elementi*. Un'ulteriore soluzione è l'uso di transizioni interattive, che facilitano l'uso dell'applicazione.
*copiando/mappando effettivamente gli elementi di dati su una vista temporanea che prende parte alla transizione, tra il suo inizio e la sua fine… ma lo spiegherò più avanti in questo articolo…
Effetto "Sbircia oltre il bordo".
L'utilizzo dell'animazione della vista a scorrimento nella sua azione trasforma l'immagine in forma 3D per un effetto cubo . Il principale fattore responsabile dell'effetto è l'offset della vista a scorrimento.
Effetto "Collega i punti".
Questa è una transizione tra le scene che trasforma l'oggetto in miniatura nell'intero schermo . Le raccolte utilizzate a questo scopo funzionano contemporaneamente, un cambiamento su una schermata corrisponde a uno spostamento sull'altra. Inoltre, quando entri nella miniatura sullo sfondo, viene visualizzato un effetto di parallasse quando scorri tra le scene.
Effetto "Sposta la forma".
L'ultimo tipo di animazione è semplice utilizzando la libreria Lottie. È l'uso più comune per animare le icone. In questo caso, queste sono le icone sulla barra delle schede. Inoltre, modificando le schede appropriate, è stata utilizzata un'animazione della transizione in una direzione specifica per intensificare ulteriormente l'effetto di interazione.
Immergiti più a fondo: il nostro primo modello di progettazione del movimento
Ora è il momento di arrivare al punto… dobbiamo andare ancora più a fondo nella struttura dei meccanismi che controllano questi esempi.
In questo articolo vi presenterò il primo pattern di motion design , che abbiamo chiamato 'Dive Deeper' con una descrizione astratta del suo utilizzo, senza entrare nei dettagli specifici. Abbiamo in programma di rendere disponibile a tutti il codice esatto e l'intero repository in futuro, senza alcuna restrizione.
L'architettura del progetto e l'applicazione di modelli di progettazione di programmazione rigorosi non sono una priorità in questo momento: ci concentriamo sulle animazioni e sulla transizione.
In questo articolo utilizzeremo due serie di funzionalità fornite per gestire le visualizzazioni durante le transizioni di scena. Pertanto, vorrei sottolineare che questo articolo è destinato a persone che hanno relativamente familiarità con UIKit e la sintassi di 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
Primo: la struttura
Per la versione base che implementa una determinata soluzione, saranno necessarie diverse classi di supporto, responsabili di fornire le informazioni necessarie sulle viste coinvolte nella transizione e di controllare la transizione stessa e le interazioni.

La classe base responsabile della gestione della transizione, il proxy, sarà TransitionAnimation. Decide in che modo avverrà la transizione e copre le funzioni standard necessarie per eseguire l'azione fornita dal team Apple.
/// Questa è una classe fondamentale per le transizioni che hanno un comportamento diverso sulla presentazione e l'eliminazione per una durata specificata. classe aperta TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { /// Indica se sta presentando o ignorando la transizione. var presentando: Bool = true /// Intervallo di tempo in cui avviene l'intera transizione. durata del permesso privato: TimeInterval /// Inizializzatore predefinito dell'animatore di transizione con valori predefiniti. /// - Durata parametro: intervallo di tempo in cui avviene l'intera transizione. /// - Presentazione del parametro: indicatore se sta presentando o ignorando la transizione. public init(duration: TimeInterval = 0.5, presenting: Bool = true) { auto.durata = durata self.presenting = presentare super.init() } /// Determina la durata della transizione. /// - Parametro transitionContext: contesto della transizione corrente. /// - Restituisce: durata specificata all'inizializzazione dell'animatore. public func transitionDuration(usando transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { restituire l'auto.durata } /// Cuore dell'animatore di transizione, in questa funzione avviene la transizione. /// - Importante: l'override di questa funzione in un tipo di transizione più concreto è fondamentale per eseguire le animazioni. /// - Parametro transitionContext: contesto di transizione. public func animateTransition (usando transitionContext: UIViewControllerContextTransitioning) { } }
Sulla base di TransitionAnimator, creiamo un file TransformTransition, il cui compito sarà quello di eseguire una transizione specifica, transizione con trasformazione (genitorialità)
/// Implementazione della transizione di trasformazione. classe aperta TransformTransition: TransitionAnimator { /// Visualizza il modello che contiene tutte le specifiche necessarie per la transizione. private var viewModel: TransformViewModel /// Inizializzatore predefinito della transizione di trasformazione. /// - Parameter viewModel: Visualizza il modello della transizione di trasformazione. /// - Durata parametro: durata della transizione. init(viewModel: TransformViewModel, durata: TimeInterval) { self.viewModel = viewModel super.init(durata: durata, presentazione: viewModel.presenting) }
La composizione della classe TransformTransition include il TransformViewModel, che, come suggerisce il nome, informa il meccanismo di quali modelli di visualizzazione si applicherà questa transizione.
/// Visualizza il modello di transizione di trasformazione che contiene le informazioni di base su di essa. classe finale TransformViewModel { /// Indica se la transizione di trasformazione sta presentando o ignorando la visualizzazione. lascia che presenti: Bool /// Matrice di modelli con specifiche sulla trasformazione per ciascuna vista. lascia modelli: [TransformModel] /// Inizializzatore predefinito del modello di visualizzazione di trasformazione. /// - Presentazione dei parametri: indica se sta presentando o ignorando la transizione di trasformazione. /// - Modelli di parametri: matrice di modelli con specifiche sulla trasformazione per ciascuna vista. init(presentazione: Bool, modelli: [TransformModel]) { self.presenting = presentare self.models = modelli } }
Il modello di trasformazione è una classe ausiliaria che descrive gli elementi specifici delle viste coinvolte nella transizione che si trovano nel genitore, solitamente le viste di un controller che possono essere trasformate.
Nel caso di una transizione, è un passaggio necessario perché questa transizione consiste nelle operazioni di viste specifiche tra determinati stati.
Secondo: l'attuazione
Estendiamo il modello di visualizzazione da cui iniziamo la transizione con Trasformabile, che ci costringe a implementare una funzione che preparerà tutti gli elementi necessari. La dimensione di questa funzione può crescere molto rapidamente, quindi ti suggerisco di scomporla in parti più piccole, ad esempio per elemento.

/// Protocollo per la classe che vuole eseguire la transizione di trasformazione. protocollo Trasformabile: ViewModel { /// Prepara modelli di viste che sono coinvolti nella transizione. /// - Parametro fromView: la vista da cui inizia la transizione /// - Parametro toView: la vista a cui va la transizione. /// - Presentazione del parametro: indica se si sta presentando o eliminando. /// - Restituisce: Matrice di strutture che contiene tutte le informazioni necessarie pronte per trasformare la transizione per ogni vista. func prepareTransitionModels(fromView: UIView, toView: UIView, presenting: Bool) -> [TransformModel] }
Il presupposto non è dire come cercare i dati delle viste che partecipano alla trasformazione. Nel mio esempio ho usato tag che rappresentano una determinata vista. Hai mano libera in questa parte dell'implementazione.
I modelli di trasformazioni di viste specifiche (TransformModel) sono il modello più piccolo dell'intero elenco. Sono costituiti da informazioni chiave sulla trasformazione come vista iniziale, vista di transizione, fotogramma iniziale, fotogramma finale, centro iniziale, centro finale, animazioni simultanee e operazione finale. La maggior parte dei parametri non deve essere utilizzata durante la trasformazione, quindi hanno i propri valori predefiniti. Per risultati minimi, è sufficiente utilizzare solo quelli necessari.
/// Inizializzatore predefinito del modello di trasformazione con valori predefiniti. /// - Parametro initialView: vista da cui inizia la transizione. /// - Parametro phantomView: vista presentata durante la transizione di trasformazione. /// - Parametro initialFrame: frame di visualizzazione che avvia la transizione di trasformazione. /// - Parametro finalFrame: frame di vista che verrà presentato alla fine della transizione di trasformazione. /// - Parametro initialCenter: necessario quando il punto di vista del centro iniziale è diverso dal centro della vista iniziale. /// - Parametro finalCenter: necessario quando il punto di vista del centro finale è diverso dal centro della vista finale. /// - Parametro parallelAnimation: animazione aggiuntiva della vista eseguita durante la transizione di trasformazione. /// - Completamento del parametro: blocco di codice attivato dopo la transizione di trasformazione. /// - Nota: è necessaria solo la vista iniziale per eseguire la versione più minimalista della transizione di trasformazione. init(initialView: UIView, phantomView: UIView = UIView(), frame iniziale: CGRect = CGRect(), frame finale: CGRect = CGRect(), initialCenter: CGPoint? = zero, finalCenter: CGPoint? = zero, parallelAnimation: (() -> Vuoto)? = zero, completamento: (() -> Vuoto)? = zero) { self.initialView = initialView self.phantomView = phantomView self.initialFrame = initialFrame self.finalFrame = finalFrame self.parallelAnimation = parallelAnimation auto.completamento = completamento self.initialCenter = initialCenter ?? CGPoint(x: frame iniziale.midX, y: frame iniziale.midY) self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY) }
La tua attenzione potrebbe essere stata catturata dalla vista fantasma. Questo è il momento in cui spiegherò il flusso di lavoro per le transizioni iOS. Nella forma più breve possibile...

Quando l'utente desidera passare alla scena successiva, iOS prepara controller specifici copiando in memoria i controller di inizio (blu) e di destinazione (verde). Successivamente, viene creato un contesto di transizione attraverso il coordinatore di transizione che contiene il contenitore, una vista "stupida" che non contiene alcuna funzione speciale, oltre a simulare le viste di transizione tra le due scene.
Il principio chiave dell'utilizzo delle transizioni è di non aggiungere alcuna vista reale al contesto di transizione, perché alla fine della transizione, tutto il contesto viene deallocato, insieme alle viste aggiunte al contenitore. Si tratta di viste che esistono solo durante la transizione e vengono quindi rimosse.
Pertanto, l'uso di viste fantasma che sono repliche di viste reali è una soluzione importante a questa transizione.

In questo caso, abbiamo una transizione che trasforma una vista in un'altra modificandone la forma e le dimensioni. Per fare ciò, all'inizio della transizione, creo una PhantomView dell'elemento dato e lo aggiungo al contenitore. FadeView è una vista ausiliaria per aggiungere morbidezza alla transizione generale.
/// Cuore della transizione di trasformazione, dove viene eseguita la transizione. Sostituzione di `TransitionAnimator.animateTransition(...)`. /// - Parametro transitionContext: il contesto della transizione di trasformazione corrente. sovrascrivi open func animateTransition (usando transitionContext: UIViewControllerContextTransitioning) { guard let toViewController = transitionContext.view(forKey: .to), let fromViewController = transitionContext.view(forKey: .from) else { restituisce Log.unexpectedState() } let containerView = transitionContext.containerView let duration =transizioneDurata(usando:transizioneContesto) let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue) let models = viewModel.models lasciate presentView = presentando ? toViewController: daViewController models.forEach {$0.initialView.isHidden = true} presentView.isHidden = vero containerView.addSubview(toViewController) se si presenta { containerView.insertSubview(fadeView, belowSubview: toViewController) } altro { containerView.addSubview(fadeView) } containerView.addSubviews(viewModel.models.map { $0.phantomView })
Nel passaggio successivo, lo trasformo nella forma di destinazione attraverso trasformazioni e, a seconda che si tratti di una presentazione o di un richiamo, esegue operazioni aggiuntive per ripulire viste specifiche: questa è l'intera ricetta per questa transizione.
let animations: () -> Void = { [sé debole] in guard let self = self else { return Log.unexpectedState() } fadeView.alpha = self.presenting.cgFloatValue models.forEach { let center = self.presenting ? $0.centrofinale: $0.centroiniziale lascia trasformare = self.presenting ? $0.presentTransform: $0.dismissTransform $0.phantomView.setTransformAndCenter(trasforma, centra) } models.compactMap { $0.parallelAnimation }.forEach { $0() } } let completamento: (Bool) -> Void = { _ in transizioneContext.completeTransition(!transitionContext.transitionWasCancelled) presentView.isHidden = falso models.compactMap { $0.completion }.forEach { $0() } models.forEach {$0.initialView.isHidden = false} if !self.presenting && transitionContext.transitionWasCancelled { toViewController.removeFromSuperview() fadeView.removeFromSuperview() } } UIView.animate(conDurata: durata, ritardo: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0,5, opzioni: .curveEaseOut, animazioni: animazioni, completamento: completamento)
Terzo: l'ingrediente speciale
Dopo aver messo insieme tutte le funzioni, classi e protocolli, il risultato dovrebbe assomigliare a questo:
La componente finale della nostra transizione sarà la sua piena interattività. A tale scopo utilizzeremo un Pan Gesture aggiunto nella vista del controller, TransitionInteractor...
/// Mediatore per gestire la transizione interattiva. classe finale TransitionInteractor: UIPercentDrivenInteractiveTransition { /// Indica se la transizione è iniziata. var hasStarted = false /// Indica se la transizione deve terminare. var shouldFinish = false }
... che inizializziamo anche nel corpo del controller.
/// Gestisce i movimenti di panoramica sugli elementi della vista raccolta e gestisce la transizione. @objc func handlePanGesture(_ gestureRecognizer: UIpanGestureRecognizer) { let percentThreshold: CGFloat = 0,1 let translation = gestureRecognizer.translation(in: view) let verticalMovement = traslazione.y / view.bounds.height let upMovement = fminf(Float(verticalMovement), 0.0) let upwardMovementPercent = fminf(abs(upwardMovement), 0.9) let progress = CGFloat(upwardMovementPercent) guard let interactor = interactionController else { return } switch gestureRecognizer.state { caso .iniziato: interactor.hasStarted = true let tapPosition = gestureRecognizer.location (in: collectionView) showDetailViewControllerDa(posizione: tapPosition) caso .cambiato: interactor.shouldFinish = progresso > percentThreshold interactor.update(progresso) caso .cancellato: interactor.hasStarted = falso interactor.cancel() caso .concluso: interactor.hasStarted = falso interactor.shouldFinish ? interactor.finish() : interactor.cancel() predefinito: rompere } }
L'interazione pronta dovrebbe essere la seguente:
Se tutto è andato secondo i piani, la nostra applicazione guadagnerà molto di più agli occhi dei suoi utenti.

Scoperto solo il picco di un iceberg
La prossima volta spiegherò l'implementazione di problemi correlati alla progettazione del movimento nelle edizioni successive.
L'applicazione, il design e il codice sorgente sono di proprietà di Miquido e sono stati creati con passione da designer e programmatori di talento per l'uso di cui non siamo responsabili nelle nostre implementazioni. Il codice sorgente dettagliato sarà disponibile in futuro tramite il nostro account github: ti invitiamo a seguirci!
Grazie per l'attenzione ea presto!