История одного вью-контроллера, который хотел показываться красиво

в 15:50, , рубрики: animation, iOS разработка, swift, разработка под iOS

Жил был скромный вью-контроллер VCYellow. И не было у него ни картинки, ни текста, ни даже малюсенькой бизнес логики. Жил он обычной вью-контроллерской жизнью.

Его товарищ вью-контроллер VCMain иногда презентовал его миру:

class VCMain: UIViewController {
...
@IBAction func onBtnTapMeTapped(_ sender: Any) {
    let vcYellow = self.storyboard!.instantiateViewController(withIdentifier: "VCYellow") as! VCYellow
    self.present(vcYellow, animated: true, completion: nil)
}

А VCYellow в свою очередь скрывался при помощи единственной кнопки "X", которой он, кстати говоря, очень гордился:

class VCYellow: UIViewController {
...
@IBAction func onBtnCloseTapped(_ sender: Any) {
    self.dismiss(animated: true, completion: nil)
}

И выглядело это не то чтобы плохо, но скучно и обыденно:

История одного вью-контроллера, который хотел показываться красиво - 1

Но была у нашего героя мечта научиться показываться и скрываться по-красоте. Да так, чтобы можно было эту красоту менять потом по праздникам или просто в честь хорошего настроения.

История одного вью-контроллера, который хотел показываться красиво - 2

Шли года… и так и осталась бы мечта мечтой, если бы не узнал VCYellow о магии под названием:

UIViewControllerTransitioningDelegate

А сила этой магии в том, что даёт она возможность подсунуть соответствующий аниматор для показа и для скрытия вью-контроллера. Как раз то, о чём мечтал наш контроллер.
Прочитал он в древних свитках как использовать заклятие и начал готовиться.
Записал себе шпаргалку с самим заклинанием, чтобы не забыть:

extension VCYellow: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AnimatorPresent(startFrame: self.startFrame)
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return AnimatorDismiss(endFrame: self.startFrame)
    }
}

В ней он тщательно расписал, что для показа нужно использовать аниматор AnimatorPresent, а при закрытии AnimatorDismiss.
Ну и в качестве помощи обоим аниматорам было решено передать фрейм главной кнопки из VCMain

А потом и сам морально настроился. Потому как без правильного настроя, как известно, никакая магия не работает:

override func viewDidLoad() {
    super.viewDidLoad()
    self.modalPresentationStyle = .custom
    self.transitioningDelegate = self
}

Попросил он своего друга VCMain презентануть себя, чтобы проверить как магия сработает и… сработала она никак…
Оказалось, что AnimatorPresent и AnimatorDismiss сами собой не появляются.

Останавливаться было уже поздно и наш герой решил создать необходимые аниматоры. Поковырялся в нужном разделе древних свитков и узнал, что для создания аниматора достаточно двух вещей.

Во-первых надо задать время, отведённое для анимации:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.3
}

а во-вторых обозначить саму анимацию:


func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    //1
    guard let vcTo = transitionContext.viewController(forKey: .to),
        let snapshot = vcTo.view.snapshotView(afterScreenUpdates: true) else {
        return
    }

    //2
    let vContainer = transitionContext.containerView

    //3
    vcTo.view.isHidden = true
    vContainer.addSubview(vcTo.view)

    //4
    snapshot.frame = self.startFrame
    vContainer.addSubview(snapshot)

    UIView.animate(withDuration: 0.3, animations: {
        //5
        snapshot.frame = (transitionContext.finalFrame(for: vcTo))
    }, completion: { success in
        //6
        vcTo.view.isHidden = false
        snapshot.removeFromSuperview()
        transitionContext.completeTransition(true)
    })
}

  1. Вытащить презентуемый вью-контроллер(в нашем случае VCYellow) и сфоткать его. Фотка нужна для упрощения анимации.
  2. Получить вьюшку, на которой будет происходить анимационное колдунство. Назовем её контекст.
  3. Нацепить вьюху конечного контроллера на контекст и скрыть её. Показать
  4. её было решено после того как закончится анимация.
  5. Подготовить фотку для анимации. Уменьшить до начальных размеров и кинуть на контекст.
  6. Расщеперить фотку на весь экран, тем самым анимировав процесс презентации.
  7. После окончания анимации показать настоящую вьюху конечного контроллера,
  8. избавиться от фотки и сообщить, что действо окончено.

В результате вышел вот такой аниматор для показа:

import UIKit

class AnimatorPresent: NSObject, UIViewControllerAnimatedTransitioning {
    let startFrame: CGRect

    init(startFrame: CGRect) {
        self.startFrame = startFrame
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.3
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let vcTo = transitionContext.viewController(forKey: .to),
        let snapshot = vcTo.view.snapshotView(afterScreenUpdates: true) else {
            return
        }

        let vContainer = transitionContext.containerView

        vcTo.view.isHidden = true
        vContainer.addSubview(vcTo.view)

        snapshot.frame = self.startFrame
        vContainer.addSubview(snapshot)

        UIView.animate(withDuration: 0.3, animations: {
            snapshot.frame = (transitionContext.finalFrame(for: vcTo))
        }, completion: { success in
            vcTo.view.isHidden = false
            snapshot.removeFromSuperview()
            transitionContext.completeTransition(true)
        })
    }
}

А после этого несложно было написать аниматор для скрывания, который делает примерно то же самое, но наоборот:

import UIKit

class AnimatorDismiss: NSObject, UIViewControllerAnimatedTransitioning {

    let endFrame: CGRect

    init(endFrame: CGRect) {
        self.endFrame = endFrame
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.3
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let vcTo = transitionContext.viewController(forKey: .to),
        let vcFrom = transitionContext.viewController(forKey: .from),
        let snapshot = vcFrom.view.snapshotView(afterScreenUpdates: true) else {
            return
        }

        let vContainer = transitionContext.containerView
        vContainer.addSubview(vcTo.view)
        vContainer.addSubview(snapshot)

        vcFrom.view.isHidden = true

        UIView.animate(withDuration: 0.3, animations: {
            snapshot.frame = self.endFrame
        }, completion: { success in
            transitionContext.completeTransition(true)
        })
    }
}

Закончив все доделки, VCYellow опять попросил своего друга VCMain презентовать себя и о чудо!

История одного вью-контроллера, который хотел показываться красиво - 3

Магия сработала! Мечта VCYellow сбылась! Теперь он может показываться и скрываться как ему захочется и ничто не будет ограничивать его фантазию!

Проект-пример можно скачать тут

Статья, которую я использовал для вдохновения находится тут

Автор: Кирилл

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js