Докажите iOS, что показать UIVIewController вы можете сами!

Сегодня я расскажу и докажу вам, что показать новый экран так, как вы этого хотите, на самом деле не так уж и сложно. Разберём простой пример, где новый экран появляется постепенно, примерно, как проявляется фотография на полароиде. Сначала рассмотрим теоретический аспект этой проблемы, затем перейдём к практике.

В процессе показа экрана есть несколько действующих лиц. Разберём, кто есть кто и за что отвечает: 1) UIViewController — главный герой в нашей «постановке», его мы будем показывать и скрывать; 2) UIViewControllerTransitioningDelegate — наш помощник, который говорит системе, что мы берём управление над показом или скрытием экрана. Каждый контроллер имеет свойство transitioningDelegate, и если оно не установлено или на запрос кастомной анимации этот делегат отвечает nil — в таком случае система использует анимации по умолчанию; 3) UIViewControllerAnimatedTransitioning — собственно, здесь представлен «сценарий» нашей презентации, как именно и как долго мы хотим, чтобы наш экран показался; 4) UIViewControllerContextTransitioning — перед началом процесса показа экрана iOS создаёт контекст, в рамках которого будет протекать весь процесс. Контекст предоставляет нам доступ к участвующим в показе контроллерам, также при окончании показа мы сообщаем ему об этом; 5) UIKIt — фреймворк, который предоставляет нам все необходимые для показа инструменты и вообще делает эту процедуру возможной.

За создание элементов 1-3 отвечает разработчик, элементы 4-5 предоставляет сама система.

Если формализовать показ нового экрана в алгоритм, то получится следующее: 1) инициация процесса (нажатие пользователем на кнопку, вызов из кода); 2) UIKit спрашивает у контроллера, который мы хотим показать, transitioningDelegate, если такового нет, то используется стандартная анимация; 3) UIKit спрашивает у transitioningDelegate объект самой анимации (класс, имплементирующий протокол UIViewControllerAnimatedTransitioning) animationController(forPresented:presenting:source:), если мы возвращаем nil, то используется стандартная анимация; 4) UIKit создаёт контекст; 5) UIKit спрашивает длительность у объекта анимации transitionDuration(using:); 6) UIKit запускает сам процесс анимации animateTransition(using:); 7) По окончанию анимации мы вызываем completeTransition(_:) у контекста.

Теперь разберём на практике

Перед тем, как приступим, предлагаю скачать стартовый проект. В нём вы найдете два ViewContoller'а, на одном есть кнопка, при нажатии на которую у нас показывается второй контроллер с фотографией милой панды. Нужно будет перейти к тэгу step_0 (git checkout step_o).

Пойдём по вышеуказанным шагам.

1. Нажатие на кнопку Show Panda.

При нажатии на кнопку у нас срабатывает segue, поэтому мы можем переопределить метод prepare(for: _, sender: _) и сделать необходимые настройки:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard segue.identifier == .showPandaSegueIdentifier,
            let destinationVC = segue.destination as? PandaViewController else {
                return
        }
    }

2. Зададим transitioningDelegate.

Зададим PandaViewController transitioningDelegate, делегатом будет выступать наш ViewController. Компилятор будет ругаться — это нормально, на следующем шаге мы устраним эту проблему. Добавим эту строчку в самый конец метода prepare(for: _, sender: _):

        destinationVC.transitioningDelegate = self

3. Создаём анимацию.

Создадим класс FadePresentAnimationContoller и унаследуем его от NSObject, а также укажем протокол UIViewControllerAnimatedTransitioning:

class FadePresentAnimationContoller: NSObject, UIViewControllerAnimatedTransitioning {

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    }
}

Сейчас наш класс ничего не делает, оставим пока так.

4. Создаём контекст.

Мы можем получить доступ к созданному контексту через параметр в функциях нашего класса FadePresentAnimationContoller.

5. Задаём длительность нашей анимации.

Укажем 1 секунду. Можете поэкспериментировать и поставить 5 секунд, можно будет насладиться нашей классной анимацией :)

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

6. Запускается процесс анимации.

Остановимся здесь подробнее. Через контекст сначала получим доступ к контроллеру, который мы хотим показать, и создадим так называемый snapshot экрана, с которого уходим. Snapshot — это просто статичный снимок всего экрана.

 guard let targetVC = transitionContext.viewController(forKey: .to),
            let snapshot = transitionContext.view(forKey: .from) else {
                return
        }

Теперь нам нужно подготовиться к презентации. В контексте создаётся UIView containerView, в это вью система уже помещает view контроллера, с которого мы уходим. Нам остаётся добавить к нему snapshot и view нового контроллера.

Также установим alpha проперти у вью в 0.0, чтобы дальше у нас была возможность его анимированно проявить:

let containerView = transitionContext.containerView
        containerView.addSubview(snapshot)
        containerView.addSubview(targetVC.view)
        targetVC.view.alpha = 0.0

Получаем длительность перехода на новый экран, чтобы подстроить саму анимацию. Выставляем alpha в 1.0 и в completion блоке нам нужно удалить snapshot — из контейнера, он нам больше не нужен.

let duration = transitionDuration(using: transitionContext)
        UIView.animate(withDuration: duration, animations: {
            targetVC.view.alpha = 1.0
        }) { _ in
            snapshot.removeFromSuperview()
        }

7. Завершаем нашу презентацию.

Нам нужно оповестить контекст, что мы закончили нашу анимацию. В конец completion-блока добавим следующую строку: transitionContext.transitionWasCancelled — она говорит нам, был ли переход отменён пользователем или нет.

transitionContext.completeTransition(!transitionContext.transitionWasCancelled)

В результате получается следующее:

let duration = transitionDuration(using: transitionContext)
        UIView.animate(withDuration: duration, animations: {
            targetVC.view.alpha = 1.0
        }) { _ in
            snapshot.removeFromSuperview()
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }