Scufundați-vă mai adânc în designul în mișcare: tranziții avansate iOS
Publicat: 2020-11-09Creativitatea aduce cu ea o dorință constantă de a impresiona utilizatorul. Timp de secole, omul a încercat să manipuleze mijloacele disponibile pentru a recrea cea mai naturală interacțiune, luând natura ca exemplu fundamental.
Când descoperă lumea, o persoană devine din ce în ce mai sensibilă la detaliile subtile ale lumii din jurul său, ceea ce îi permite să distingă instinctiv artificialitatea de ființele vii. Această linie este estompată odată cu dezvoltarea tehnologiei, în care software-ul își propune să creeze un mediu în care utilizatorul său își descrie experiența într-o lume creată artificial ca fiind naturală.

Ecou naturii în designul aplicației
Acest articol va introduce procesul de estompare a graniței folosind exemplul transformării formei în animația interactivă a elementelor de zi cu zi în majoritatea aplicațiilor iOS. O modalitate de a imita natura este prin diferite transformări ale poziției unui obiect în timp. Exemple de funcții ale timpului de animație sunt prezentate mai jos.

Combinat cu utilizarea corectă a timpului prin adăugarea unei transformări geometrice, putem obține un număr infinit de efecte . Ca o demonstrație a posibilităților de design și tehnologie de astăzi, a fost creată aplicația Motion Patterns, care include soluții populare, dezvoltate de compania noastră de dezvoltare software. Deoarece nu sunt scriitor, ci programator și nimic nu vorbește mai bine decât exemplele vii, nu am de ales decât să vă invit în această lume minunată!
Mostre de descoperit

Să aruncăm o privire la modul în care designul în mișcare poate transforma un design standard în ceva excepțional! În exemplele de mai jos, în partea stângă există o aplicație care folosește doar animații iOS de bază, în timp ce în partea dreaptă există o versiune a aceleiași aplicații cu unele îmbunătățiri.
Efectul „Dive Deeper”.
Aceasta este o tranziție folosind transformarea între două stări ale unei vederi . Construit pe baza unei colecții, după selectarea unei anumite celule, trecerea la detaliile unui element are loc prin transformarea elementelor sale individuale *. O soluție suplimentară este utilizarea tranzițiilor interactive, care facilitează utilizarea aplicației.
*de fapt copierea/cartarea elementelor de date pe o vizualizare temporară care participă la tranziția, între începutul și sfârșitul acesteia... dar voi explica acest lucru mai târziu în acest articol...
Efectul „Peek Over the Edge”.
Folosind animația de vizualizare de defilare în acțiunea sa transformă imaginea sub formă de 3D pentru un efect de cub . Principalul factor responsabil pentru efect este offset-ul vizualizării de defilare.
Efectul „Conectează punctele”.
Aceasta este o tranziție între scene care transformă obiectul în miniatură în întregul ecran . Colecțiile utilizate în acest scop funcționează concomitent, o schimbare pe un ecran corespunde unei schimbări pe celălalt. În plus, când introduceți miniatura în fundal, apare un efect de paralaxă când treceți între scene.
Efectul „Shift the Shape”.
Ultimul tip de animație este unul simplu folosind biblioteca Lottie. Este cea mai comună utilizare pentru animarea pictogramelor. În acest caz, acestea sunt pictogramele din bara de file. În plus, prin schimbarea filelor corespunzătoare, a fost folosită o animație a tranziției într-o direcție specifică pentru a intensifica și mai mult efectul de interacțiune.
Scufundați-vă mai adânc: primul nostru model de design în mișcare
Acum este timpul să ajungem la subiect... trebuie să mergem și mai adânc în structura mecanismelor care controlează aceste exemple.
În acest articol, vă voi prezenta primul model de design în mișcare , pe care l-am numit „Dive Deeper” cu o descriere abstractă a utilizării sale, fără a intra în detalii specifice. Intenționăm să punem codul exact și întregul depozit disponibil pentru toată lumea în viitor, fără nicio restricție.
Arhitectura proiectului și modelele stricte de proiectare de programare aplicate nu sunt o prioritate în acest moment - ne concentrăm pe animații și tranziție.
În acest articol, vom folosi două seturi de caracteristici care sunt furnizate pentru a gestiona vizualizările în timpul tranzițiilor scenei. Astfel, aș dori să subliniez că acest articol este destinat persoanelor care sunt relativ familiarizate cu UIKit și sintaxa 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
În primul rând: structura
Pentru versiunea de bază care implementează o soluție dată, vor fi necesare mai multe clase helper, responsabile de furnizarea informațiilor necesare despre vederile implicate în tranziție și controlul tranziției în sine și a interacțiunilor.

Clasa de bază responsabilă cu gestionarea tranziției, proxy-ul, va fi TransitionAnimation. Acesta decide în ce mod va avea loc tranziția și acoperă funcțiile standard necesare pentru a efectua acțiunea oferită de echipa Apple.
/// Aceasta este o clasă fundamentală pentru tranzițiile care au un comportament diferit la prezentare și închidere pe durata specificată. clasa deschisă TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { /// Indicând dacă prezintă sau respinge tranziția. var prezentând: Bool = adevărat /// Interval de timp în care are loc întreaga tranziție. durata let privat: TimeInterval /// Inițializatorul implicit al animatorului de tranziție cu valorile implicite. /// - Durata parametrului: Interval de timp în care are loc întreaga tranziție. /// - Prezentarea parametrului: Indicator dacă prezintă sau închide tranziția. public init(durată: TimeInterval = 0,5, prezentând: Bool = adevărat) { self.duration = durata self.presenting = prezentare super.init() } /// Determină durata tranziției. /// - Parametru transitionContext: Contextul tranziției curente. /// - Returnează: Durata specificată la inițializarea animatorului. public func transitionDuration(folosind transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return self.durata } /// Inima animatorului de tranziție, în această funcție are loc tranziția. /// - Important: Suprascrierea acestei funcții într-un tip mai concret de tranziție este crucială pentru a efectua animații. /// - Parametru transitionContext: Contextul tranziției. public func animateTransition(folosind transitionContext: UIViewControllerContextTransitioning) { } }
Pe baza TransitionAnimator, creăm un fișier TransformTransition, a cărui sarcină va fi să efectuăm o anumită tranziție, tranziție cu transformare (parenting)
/// Implementarea tranziției de transformare. clasă deschisă TransformTransition: TransitionAnimator { /// Vizualizați modelul care conține toate specificațiile necesare pentru tranziție. private var viewModel: TransformViewModel /// Inițializatorul implicit al tranziției transformării. /// - Parametru viewModel: Vedeți modelul tranziției transformării. /// - Durata parametrului: Durata tranziției. init(viewModel: TransformViewModel, duration: TimeInterval) { self.viewModel = viewModel super.init(durata: durata, prezentarea: viewModel.presenting) }
Compoziția clasei TransformTransition include TransformViewModel, care, după cum sugerează și numele, informează mecanismul asupra modelelor de vizualizare cărora se va aplica această tranziție.
/// Vizualizați modelul de tranziție de transformare care conține informații de bază despre acesta. clasa finală TransformViewModel { /// Indică dacă tranziția de transformare prezintă sau închide vizualizarea. lasă prezentarea: Bool /// Matrice de modele cu specificații despre transformare pentru fiecare vizualizare. lasă modelele: [TransformModel] /// Inițializator implicit al modelului de vizualizare de transformare. /// - Prezentarea parametrului: indică dacă prezintă sau respinge tranziția de transformare. /// - Modele cu parametri: Matrice de modele cu specificații despre transformare pentru fiecare vizualizare. init(prezentând: Bool, modele: [TransformModel]) { self.presenting = prezentare self.models = modele } }
Modelul de transformare este o clasă auxiliară care descrie elementele specifice ale vederilor implicate în tranziție situate în părinte, de obicei vederile unui controler care pot fi transformate.
În cazul unei tranziții, este un pas necesar deoarece această tranziție constă în operațiuni de vederi specifice între stări date.
În al doilea rând: implementarea
Extindem modelul de vizualizare de la care începem tranziția cu Transformable, ceea ce ne obligă să implementăm o funcție care să pregătească toate elementele necesare. Dimensiunea acestei funcții poate crește foarte repede, așa că vă sugerez să o descompuneți în părți mai mici, de exemplu pe element.
/// Protocol pentru clasa care dorește să efectueze tranziția de transformare. protocol Transformabil: ViewModel { /// Pregătește modele de vederi care sunt implicate în tranziție. /// - Parametru fromView: vizualizarea de la care începe tranziția /// - Parametrul toView: vizualizarea la care trece tranziția. /// - Prezentarea parametrului: indică dacă se prezintă sau se respinge. /// - Returnări: Matrice de structuri care deține toate informațiile necesare gata pentru a transforma tranziția pentru fiecare vizualizare. func prepareTransitionModels(fromView: UIView, toView: UIView, prezenting: Bool) -> [TransformModel] }
Presupunerea nu este de a spune cum să căutați datele vederilor care participă la transformare. În exemplul meu, am folosit etichete care reprezintă o anumită vedere. Aveți mână liberă în această parte a implementării.

Modelele de transformări specifice de vedere (TransformModel) sunt cel mai mic model din întreaga listă. Acestea constau în informații cheie de transformare, cum ar fi vizualizarea de început, vizualizarea de tranziție, cadru de început, cadru de sfârșit, centrul de început, centrul de sfârșit, animații concomitente și operarea de final. Majoritatea parametrilor nu trebuie utilizați în timpul transformării, deci au propriile valori implicite. Pentru rezultate minime, este suficient să folosiți numai cele necesare.
/// Inițializator implicit al modelului de transformare cu valorile implicite. /// - Parametru initialView: Vedere de la care începe tranziția. /// - Parametru phantomView: Vizualizare care este prezentată în timpul tranziției transformării. /// - Parametru initialFrame: Cadrul de vedere care începe tranziția de transformare. /// - Parametru finalFrame: Cadru de vedere care va fi prezentat la sfârșitul tranziției transformării. /// - Parametru initialCenter: Este necesar când punctul de vedere central inițial este diferit de centrul vederii inițiale. /// - Parametru finalCenter: Este necesar când punctul de vedere central final este diferit de centrul vederii finale. /// - Parametru parallelAnimation: Animație suplimentară a vizualizării efectuată în timpul tranziției transformării. /// - Finalizarea parametrilor: bloc de cod declanșat după tranziția de transformare. /// - Notă: Este necesară doar vizualizarea inițială pentru a realiza cea mai minimalistă versiune a tranziției de transformare. init(initialView: UIView, phantomView: UIView = UIView(), initialFrame: CGRect = CGRect(), finalFrame: CGRect = CGRect(), initialCenter: CGPoint? = zero, finalCenter: CGPoint? = zero, parallelAnimation: (() -> Void)? = zero, completare: (() -> Void)? = zero) { self.initialView = initialView self.phantomView = phantomView self.initialFrame = initialFrame self.finalFrame = finalFrame self.parallelAnimation = parallelAnimation self.completion = completare self.initialCenter = initialCenter ?? CGPoint(x: initialFrame.midX, y: initialFrame.midY) self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY) }
Este posibil ca atenția dvs. să fi fost captată de vizualizarea fantomă. Acesta este momentul în care voi explica fluxul de lucru pentru tranzițiile iOS. În cea mai scurtă formă posibilă...

Când utilizatorul dorește să treacă la scena următoare, iOS pregătește controlere specifice prin copierea controlerelor de pornire (albastru) și țintă (verde) în memorie. În continuare, se creează un context de tranziție prin coordonatorul de tranziție care conține containerul, o vedere „prost” care nu conține nicio funcție specială, pe lângă simularea vederilor de tranziție între cele două scene.
Principiul cheie al lucrului cu tranziții este de a nu adăuga nicio vedere reală contextului de tranziție, deoarece la sfârșitul tranziției, tot contextul este dealocat, împreună cu vizualizările adăugate în container. Acestea sunt vederi care există doar în timpul tranziției și apoi sunt eliminate.
Prin urmare, utilizarea vederilor fantomă care sunt replici ale vederilor reale este o soluție importantă pentru această tranziție.

În acest caz, avem o tranziție care transformă o vedere în alta schimbându-i forma și dimensiunea. Pentru a face acest lucru, la începutul tranziției, creez un PhantomView al elementului dat și îl adaug în container. FadeView este o vizualizare auxiliară pentru a adăuga moliciune tranziției generale.
/// Inima tranziției transformării, unde se efectuează tranziția. Suprascrierea `TransitionAnimator.animateTransition(...)`. /// - Parametru transitionContext: contextul tranziției transformării curente. suprascrie funcția deschisă animateTransition (folosind transitionContext: UIViewControllerContextTransitioning) { guard let toViewController = transitionContext.view(forKey: .to), let fromViewController = transitionContext.view(forKey: .from) else { returnează Log.uneexpectedState() } let containerView = transitionContext.containerView let duration = transitionDuration (folosind: transitionContext) let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue) let modele = viewModel.models let presentedView = prezentare ? toViewController : fromViewController models.forEach { $0.initialView.isHidden = adevărat } presentedView.isHidden = adevărat containerView.addSubview(toViewController) dacă prezintă { containerView.insertSubview(fadeView, belowSubview: toViewController) } altfel { containerView.addSubview(fadeView) } containerView.addSubviews(viewModel.models.map { $0.phantomView })
În pasul următor, o transform în forma țintă prin transformări și, în funcție de faptul că este o prezentare sau o rechemare, efectuează operații suplimentare pentru a curăța vederi specifice - aceasta este întreaga rețetă pentru această tranziție.
lasă animațiile: () -> Void = { [eu slab] în guard let self = self else { return Log.uneexpectedState() } fadeView.alpha = self.presenting.cgFloatValue modele.forEach { lasa centru = self.presenting ? $0.finalCenter : $0.initialCenter let transform = self.presenting ? $0.presentTransform : $0.dismissTransform $0.phantomView.setTransformAndCenter(transform, center) } models.compactMap { $0.parallelAnimation }.forEach { $0() } } lasă completarea: (Bool) -> Void = { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) presentedView.isHidden = fals models.compactMap { $0.completion }.forEach { $0() } models.forEach { $0.initialView.isHidden = false } dacă !self.prezenting && transitionContext.transitionWasCancelled { toViewController.removeFromSuperview() fadeView.removeFromSuperview() } } UIView.animate(withDuration: duration, întârziere: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0,5, opțiuni: .curveEaseOut, animatii: animatii, completare: finalizare)
În al treilea rând: ingredientul special
După ce ați reunit toate funcțiile, clasele și protocoalele, rezultatul ar trebui să arate astfel:
Componenta finală a tranziției noastre va fi interactivitatea sa deplină. În acest scop, vom folosi un Pan Gesture adăugat în vizualizarea controlerului, TransitionInteractor...
/// Mediator pentru a gestiona tranziția interactivă. Clasa finală TransitionInteractor: UIPercentDrivenInteractiveTransition { /// Indică dacă tranziția a început. var hasStarted = fals /// Indică dacă tranziția ar trebui să se termine. var shouldFinish = false }
… pe care le inițializam și în corpul controlerului.
/// Gestionează gestul de pan pentru elementele din vizualizarea colecției și gestionează tranziția. @objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) { lasă percentThreshold: CGFloat = 0,1 let translation = gestureRecognizer.translation(in: view) lasă verticalMovement = translation.y / view.bounds.height let upwardMovement = fminf(Float (Mișcare verticală), 0,0) let upwardMovementPercent = fminf(abs(upwardMovement), 0,9) lasă progresul = CGFloat(upwardMovementPercent) guard let interactor = interactionController else { return } comutați gestureRecognizer.state { cazul .a inceput: interactor.hasStarted = adevărat let tapPosition = gestureRecognizer.location(în: collectionView) showDetailViewControllerFrom(locație: tapPosition) caz .schimbat: interactor.shouldFinish = progres > prag procent interactor.update(progres) caz .anulat: interactor.hasStarted = fals interactor.cancel() caz .terminat: interactor.hasStarted = fals interactor.shouldFinish ? interactor.finish() : interactor.cancel() Mod implicit: pauză } }
Interacțiunea gata ar trebui să fie după cum urmează:
Dacă totul a decurs conform planului, aplicația noastră va câștiga mult mai mult în ochii utilizatorilor săi.

A descoperit doar un vârf de aisberg
Data viitoare, voi explica implementarea problemelor conexe ale designului în mișcare în edițiile următoare.
Aplicația, designul și codul sursă sunt proprietatea Miquido și au fost create cu pasiune de designeri și programatori talentați pentru utilizarea cărora nu suntem responsabili în implementările noastre. Codul sursă detaliat va fi disponibil în viitor prin contul nostru github - vă invităm să ne urmăriți!
Vă mulțumim pentru atenție și ne vedem curând!