Zanurz się głębiej w projektowanie ruchu: zaawansowane przejścia na iOS

Opublikowany: 2020-11-09

Kreatywność niesie ze sobą ciągłą chęć zaimponowania użytkownikowi. Od wieków człowiek próbował manipulować dostępnymi środkami, aby odtworzyć najbardziej naturalne interakcje, biorąc za podstawowy przykład naturę.

Odkrywając świat człowiek staje się coraz bardziej wrażliwy na subtelne szczegóły otaczającego go świata, co pozwala mu instynktownie odróżnić sztuczność od żywych istot. Linia ta zaciera się wraz z rozwojem technologii, gdzie oprogramowanie ma na celu stworzenie środowiska, w którym jego użytkownik opisuje swoje doświadczenia w sztucznie stworzonym świecie jako naturalne.

Echa natury w projektowaniu aplikacji

W tym artykule przedstawimy proces zacierania granicy na przykładzie transformacji kształtu w interaktywnej animacji elementów codziennego użytku w większości aplikacji iOS. Jednym ze sposobów naśladowania natury są różne przekształcenia położenia obiektu w czasie. Poniżej przedstawiono przykładowe funkcje czasu animacji.

W połączeniu z odpowiednim wykorzystaniem czasu poprzez dodanie transformacji geometrycznej, możemy uzyskać nieskończoną ilość efektów . Jako demonstrację możliwości dzisiejszego projektowania i technologii stworzona została aplikacja Motion Patterns, w skład której wchodzą popularne rozwiązania opracowane przez naszą firmę programistyczną. Ponieważ nie jestem pisarzem, ale programistą i nic nie przemawia lepiej niż żywe przykłady, nie mam innego wyjścia, jak tylko zaprosić Cię do tego wspaniałego świata!

Próbki do odkrycia

Przyjrzyjmy się, jak projektowanie ruchu może przekształcić zwykły projekt w coś wyjątkowego! W poniższych przykładach po lewej stronie znajduje się aplikacja wykorzystująca tylko podstawowe animacje iOS, a po prawej wersja tej samej aplikacji z pewnymi ulepszeniami.

Efekt „nurkuj głębiej”

Jest to przejście wykorzystujące transformację pomiędzy dwoma stanami widoku . Zbudowany na bazie kolekcji, po wybraniu konkretnej komórki, przejście do szczegółów elementu następuje poprzez przekształcenie jego poszczególnych elementów*. Dodatkowym rozwiązaniem jest zastosowanie interaktywnych przejść, które ułatwiają korzystanie z aplikacji.

*właściwie kopiowanie/mapowanie elementów danych na tymczasowym widoku biorącym udział w przejściu, między jego początkiem a jego końcem… ale wyjaśnię to w dalszej części artykułu…

Efekt „Peek Over the Edge”

Użycie animacji widoku przewijania w jego akcji przekształca obraz w formę 3D na efekt sześcianu . Głównym czynnikiem odpowiedzialnym za efekt jest przesunięcie widoku przewijania.

Efekt „Połącz kropki”

Jest to przejście między scenami, które przekształca miniaturowy obiekt na cały ekran . Wykorzystywane do tego celu kolekcje działają równolegle, zmiana na jednym ekranie odpowiada przesunięciu na drugim. Dodatkowo, gdy wejdziesz w miniaturę w tle, podczas przewijania między scenami pojawia się efekt paralaksy.

Efekt „Zmień kształt”

Ostatni typ animacji to prosta animacja wykorzystująca bibliotekę Lottie. Jest to najczęstsze zastosowanie do animowania ikon. W tym przypadku są to ikony na pasku kart. Dodatkowo zmieniając odpowiednie zakładki wykorzystano animację przejścia w określonym kierunku, aby jeszcze bardziej zintensyfikować efekt interakcji.

Zanurz się głębiej: nasz pierwszy wzorzec projektowania ruchu

Teraz pora przejść do sedna… musimy jeszcze głębiej zagłębić się w strukturę mechanizmów kontrolujących te przykłady.

W tym artykule przedstawię pierwszy wzorzec projektowania ruchu , który nazwaliśmy „Dive Deeper” z abstrakcyjnym opisem jego użycia, bez wchodzenia w szczegóły. Planujemy w przyszłości udostępnić wszystkim dokładny kod i całe repozytorium, bez żadnych ograniczeń.

Architektura projektu i zastosowane ścisłe wzorce projektowe programowania nie są w tej chwili priorytetem — skupiamy się na animacjach i przejściach.

W tym artykule użyjemy dwóch zestawów funkcji, które są dostarczane do zarządzania widokami podczas przejść scen. W związku z tym chciałbym zaznaczyć, że ten artykuł jest przeznaczony dla osób, które stosunkowo dobrze znają UIKit i składnię 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

Po pierwsze: struktura

W przypadku podstawowej wersji implementującej dane rozwiązanie, potrzebnych będzie kilka klas pomocniczych, odpowiedzialnych za dostarczanie niezbędnych informacji o widokach zaangażowanych w przejście oraz kontrolowanie samego przejścia i interakcji.

Przejście w Swift

Podstawową klasą odpowiedzialną za zarządzanie przejściem, proxy, będzie TransitionAnimation. Decyduje, w jaki sposób nastąpi przejście i obejmuje standardowe funkcje potrzebne do wykonania akcji dostarczonej przez zespół Apple.

 /// To jest podstawowa klasa dla przejść, które zachowują się inaczej podczas prezentowania i odrzucania w określonym czasie.
otwarta klasa TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// Wskazuje, czy przedstawia lub odrzuca przejście.
    zmienna prezentująca: Bool = prawda
    
    /// Przedział czasu, w którym następuje całe przejście.
    czas trwania najmu prywatnego: TimeInterval
    
    /// Domyślny inicjator animatora przejścia z wartościami domyślnymi.
    /// - Czas trwania parametru: Przedział czasu, w którym następuje całe przejście.
    /// - Prezentacja parametru: Wskazuje, czy przedstawia lub odrzuca przejście.
    public init(duration: TimeInterval = 0.5, prezentując: Bool = true) {
        czas trwania własnego = czas trwania
        self.presenting = prezentacja
        super.init()
    }
    
    /// Określa czas trwania przejścia.
    /// - Parametr transitionContext: Kontekst aktualnego przejścia.
    /// - Zwraca: Określony czas trwania przy inicjalizacji animatora.
    public func transitionDuration(używając transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        zwróć sam.czas trwania
    }
    
    /// Serce animatora przejścia, w tej funkcji następuje przejście.
    /// - Ważne: Zastąpienie tej funkcji w bardziej konkretnym typie przejścia ma kluczowe znaczenie dla wykonywania animacji.
    /// - Parametr transitionContext: Kontekst przejścia.
    public func animateTransition(używając transitionContext: UIViewControllerContextTransitioning) { }
    
}

Na podstawie TransitionAnimator tworzymy plik TransformTransition, którego zadaniem będzie wykonanie określonego przejścia, przejścia z transformacją (rodzicielstwo)

 /// Implementacja przejścia transformacji.
otwarta klasa TransformTransition: TransitionAnimator {
    
    /// Zobacz model, który zawiera wszystkie potrzebne specyfikacje przejścia.
    private var viewModel: TransformViewModel
    
    /// Domyślny inicjator przejścia transformacji.
    /// - Parametr viewModel: Zobacz model przejścia transformacji.
    /// - Czas trwania parametru: Czas trwania przejścia.
    init(viewModel: TransformViewModel, czas trwania: TimeInterval) {
        self.viewModel = viewModel
        super.init(czas trwania: czas trwania, prezentacja: viewModel.presenting)
    }

Skład klasy TransformTransition zawiera TransformViewModel, który, jak sama nazwa wskazuje, informuje o mechanizmie, do jakich modeli widoku będzie miało zastosowanie to przejście.

 /// Zobacz model przejścia transformacyjnego, który zawiera podstawowe informacje o nim.
końcowa klasa TransformViewModel {
    
    /// Wskazuje, czy przejście transformacji przedstawia lub odrzuca widok.
    pozwól prezentować: Bool
    /// Tablica modeli ze specyfikacją transformacji dla każdego widoku.
    niech modele: [TransformModel]
    
    /// Domyślny inicjator modelu widoku transformacji.
    /// - Prezentacja parametru: Wskazuje, czy przedstawia, czy odrzuca przejście transformacji.
    /// - Modele parametrów: Tablica modeli ze specyfikacją transformacji dla każdego widoku.
    init(prezentowanie: Bool, modele: [TransformModel]) {
        self.presenting = prezentacja
        self.models = modelki
    }
    
}

Model transformacji jest klasą pomocniczą, która opisuje konkretne elementy widoków biorących udział w przejściu znajdujących się w rodzicu, zwykle widoki kontrolera, które mogą być transformowane.

W przypadku przejścia jest to krok konieczny, ponieważ przejście to polega na operacjach określonych widoków pomiędzy danymi stanami.

Po drugie: wdrożenie

Rozszerzamy model widoku, od którego zaczynamy przejście o Transformable, co wymusza na nas zaimplementowanie funkcji, która przygotuje wszystkie niezbędne elementy. Rozmiar tej funkcji może bardzo szybko rosnąć, więc proponuję rozbić ją na mniejsze części, na przykład na element.

 /// Protokół dla klasy, która chce wykonać przejście transformacji.
protokół Transformowalny: ViewModel {
    
    /// Przygotowuje modele widoków biorących udział w przejściu.
    /// - Parametr fromView: Widok, z którego rozpoczyna się przejście
    /// - Parametr toView: Widok, do którego przechodzi przejście.
    /// - Prezentacja parametru: Wskazuje, czy jest prezentowany, czy odrzucany.
    /// - Zwraca: Tablica struktur, która przechowuje wszystkie potrzebne informacje gotowe do przekształcenia przejścia dla każdego widoku.
    func PrepareTransitionModels(fromView: UIView, toView: UIView, prezentacja: Bool) -> [TransformModel]
    
}

Założeniem nie jest mówienie, jak szukać danych widoków uczestniczących w transformacji. W moim przykładzie użyłem tagów, które reprezentują dany widok. Masz wolną rękę w tej części wdrożenia.

Modele poszczególnych przekształceń widoków (TransformModel) są najmniejszym modelem na całej liście. Składają się z kluczowych informacji o transformacji, takich jak widok początkowy, widok przejścia, klatka początkowa, klatka końcowa, środek początkowy, środek końcowy, współbieżne animacje i operacja końcowa. Większość parametrów nie musi być używana podczas transformacji, więc mają swoje własne wartości domyślne. Aby uzyskać minimalne rezultaty, wystarczy użyć tylko tych, które są wymagane.

 /// Domyślny inicjator modelu transformacji z wartościami domyślnymi.
    /// - Parametr initialView: Widok, z którego rozpoczyna się przejście.
    /// - Parametr phantomView: Widok, który jest prezentowany podczas przejścia transformacji.
    /// - Parametr initialFrame: Ramka widoku rozpoczynająca przejście transformacji.
    /// - Parametr finalFrame: Ramka widoku, która będzie prezentowana na końcu przejścia transformacji.
    /// - Parametr initialCenter: Niezbędny, gdy początkowy środkowy punkt widzenia jest inny niż początkowy środek widoku.
    /// - Parametr finalCenter: Potrzebny, gdy końcowy środek widoku różni się od środka końcowego widoku.
    /// - Parametr parallelAnimation: Dodatkowa animacja widoku wykonywana podczas przejścia transformacji.
    /// - Uzupełnianie parametrów: blok kodu wyzwalany po przejściu transformacji.
    /// - Uwaga: Do wykonania najbardziej minimalistycznej wersji przejścia transformacji potrzebny jest tylko widok początkowy.
    init(initialView: UIView,
         widmowy widok: UIView = UIView(),
         InitialFrame: CGRect = CGRect(),
         finalFrame: CGRect = CGRect(),
         InitialCenter: CGPoint? = zero,
         finalCenter: CGPoint? = zero,
         ParallelAnimation: (() -> Void)? = zero,
         zakończenie: (() -> Unieważnienie)? = zero) {
        self.initialView = InitialView
        self.phantomView = fantomView
        self.initialFrame = InitialFrame
        self.finalFrame = finalFrame
        self.parallelAnimation = równoległaAnimacja
        samoukończenie = ukończenie
        self.initialCenter = InitialCenter ?? CGPoint(x: ramka początkowa.midX, y: ramka początkowa.midY)
        self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    }

Twoja uwaga mogła zostać przyciągnięta przez widok fantomowy. To jest moment, w którym wyjaśnię workflow dla przejść iOS. W możliwie najkrótszej formie…

Przepływ pracy dla przejść na iOS

Gdy użytkownik chce przejść do następnej sceny, iOS przygotowuje określone kontrolery, kopiując do pamięci kontroler startowy (niebieski) i docelowy (zielony). Następnie kontekst przejścia jest tworzony przez koordynatora przejścia, który zawiera kontener, „głupi” widok, który nie zawiera żadnej specjalnej funkcji, poza symulacją widoków przejścia między dwiema scenami.

Kluczową zasadą pracy z przejściami jest nie dodawanie żadnego rzeczywistego widoku do kontekstu przejścia, ponieważ pod koniec przejścia cały kontekst jest usuwany wraz z widokami dodanymi do kontenera. Są to widoki, które istnieją tylko podczas przejścia, a następnie są usuwane.

Dlatego użycie widoków fantomowych, które są replikami widoków rzeczywistych, jest ważnym rozwiązaniem tego przejścia.

W tym przypadku mamy przejście, które przekształca jeden widok w inny, zmieniając jego kształt i rozmiar. W tym celu na początku przejścia tworzę PhantomView danego elementu i dodaję go do kontenera. FadeView to widok pomocniczy, który dodaje miękkości całemu przejściu.

 /// Serce przejścia transformacji, w którym następuje przejście. Zastępowanie `TransitionAnimator.animateTransition(...)`.
    /// - Parametr transitionContext: kontekst aktualnego przejścia transformacji.
    override open func animateTransition(używając transitionContext: UIViewControllerContextTransitioning) {
        guard let toViewController = transitionContext.view(forKey: .to),
            let fromViewController = transitionContext.view(forKey: .from) else {
                return Log.unexpectedState()
        }
        let containerView = transitionContext.containerView
        niech czas trwania = transitionDuration (używając: transitionContext)
        let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue)
        let modele = viewModel.models
        niech presentView = przedstawia ? toViewController : fromViewController
        
        modele.forEach { $0.initialView.isHidden = prawda }
        PresentView.isHidden = prawda
        containerView.addSubview(toViewController)
        jeśli przedstawiasz {
            containerView.insertSubview(fadeView, belowSubview: toViewController)
        } w przeciwnym razie {
            containerView.addSubview(fadeView)
        }
        containerView.addSubviews(viewModel.models.map { $0.phantomView })

W kolejnym kroku przekształcam go do kształtu docelowego poprzez przekształcenia i w zależności od tego, czy jest to prezentacja, czy przypomnienie, wykonuje dodatkowe operacje w celu oczyszczenia konkretnych widoków – to cała recepta na to przejście.

 niech animacje: () -> Void = { [słabe ja] in
            guard let self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            modele.dla każdego {
                let center = self.presenting ? $0.finalCenter : $0.initialCenter
                niech transform = self.presenting ? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(transformacja, środek)
            }
            modele.compactMap { $0.parallelAnimation }.forEach { $0() }
        }
        
        let dokończenie: (Bool) -> Void = { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            presentView.isHidden = false
            models.compactMap { $0.completion }.forEach { $0() }
            modele.forEach { $0.initialView.isHidden = false }
            if !self.presenting && transitionContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration: czas trwania,
                       opóźnienie: 0,
                       za pomocą sprężyny z tłumieniem: 1,
                       początkowaSprężynaPrędkość: 0.5,
                       opcje: .curveEaseOut,
                       animacje: animacje,
                       ukończenie: ukończenie)

Po trzecie: specjalny składnik

Po złożeniu wszystkich funkcji, klas i protokołów wynik powinien wyglądać tak:

Ostatnim elementem naszego przejścia będzie pełna interaktywność. W tym celu wykorzystamy gest Pan dodany w widoku kontrolera, TransitionInteractor…

 /// Mediator do obsługi przejścia interaktywnego.
końcowa klasa TransitionInteractor: UIPercentDrivenInteractiveTransition {
    
    /// Wskazuje, czy rozpoczęło się przejście.
    var hasStarted = false
    /// Wskazuje, czy przejście powinno się zakończyć.
    var shouldFinish = false

}

… które również inicjujemy w ciele kontrolera.

 /// Obsługuje gest panoramowania na elementach widoku kolekcji i zarządza przejściami.
    @objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
        niech procent Próg: CGFloat = 0,1
        niech tłumaczenie = gestureRecognizer.translation(w: view)
        niech verticalMovement = translation.y / view.bounds.height
        niech ruch w górę = fminf(Pływak(Ruch w pionie), 0.0)
        let ProcentRuchu w górę = fminf(abs(Ruch w górę), 0.9)
        niech postęp = CGFloat(upwardMovementPercent)
        guard let interactor = interactController else { return }
        przełącz gestRecognizer.state {
        sprawa .rozpoczęła się:
            interactor.hasStarted = prawda
            niech tapPosition = gestureRecognizer.location(w: collectionView)
            showDetailViewControllerFrom(lokalizacja: tapPosition)
        wielkość liter .zmieniona:
            interactor.shouldFinish = postęp > procent Próg
            interactor.update(postęp)
        sprawa .anulowana:
            interactor.hasStarted = false
            interaktywny.anuluj()
        sprawa zakończona:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? interaktywny.zakończ()
                : interaktywny.anuluj()
        domyślna:
            przerwanie
        }
    }

Gotowa interakcja powinna wyglądać następująco:

Jeśli wszystko poszło zgodnie z planem, nasza aplikacja zyska w oczach użytkowników znacznie więcej.

Odkryto tylko szczyt góry lodowej

Następnym razem wyjaśnię realizację pokrewnych zagadnień projektowania ruchu w kolejnych wydaniach.

Aplikacja, design oraz kod źródłowy są własnością firmy Miquido, a zostały stworzone z pasją przez utalentowanych projektantów i programistów, za wykorzystanie których nie ponosimy odpowiedzialności w naszych wdrożeniach. Szczegółowy kod źródłowy będzie dostępny w przyszłości za pośrednictwem naszego konta github — zapraszamy do śledzenia nas!

Dziękuję za uwagę i do zobaczenia wkrótce!