Докажите 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'а, на одном есть кнопка, при нажатии на которую у нас показывается второй контроллер с фотографией милой панды. Нужно будет перейти к тэгу
Пойдём по вышеуказанным шагам.
1. Нажатие на кнопку Show Panda.
При нажатии на кнопку у нас срабатывает segue, поэтому мы можем переопределить метод
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. Компилятор будет ругаться — это нормально, на следующем шаге мы устраним эту проблему. Добавим эту строчку в самый конец метода
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) }