Привет, Хабр! Всем нравятся отзывчивые приложения. Ещё лучше, когда в них есть уместные анимации. В этой статье я расскажу и покажу со всем «мясом», как правильно показывать, скрывать, крутить, вертеть и делать всякое с всплывающими экранами.

Изначально я хотел написать статью о том, что на iOS 10 появился удобный
UIViewPropertyAnimator, который решает проблему прерываемых анимаций. Теперь их можно будет остановить, инвертировать, продолжить или отменить. Эпл называет такой интерфейс Fluid.Но потом я понял: сложно рассказывать о прерывании анимации контроллеров без описания того, как эти переходы правильно анимировать. Поэтому будет две статьи. В этой разберёмся, как правильно показывать и скрывать экран, а о прерывании — в следующей (но самые нетерпеливые уже могут посмотреть пример).
Как работают транзишены
У UIViewController есть проперти transitioningDelegate. Это протокол с разными функциями, каждая возвращает объект:
animationControllerза анимацию,interactionControllerза прерывание анимаций,presentationControllerза отображение: иерархию, frame и т.д.

На основе всего этого сделаем всплывающую панель:

Готовим контроллеры
Можно анимировать переход для модальных контроллеров и для UINavigationController (работает через UINavigationControllerDelegate).
Мы будет рассматривать модальные переходы. Показываем контроллер как обычно:
class ParentViewController: UIViewController { @IBAction func openDidPress(_ sender: Any) { let child = ChildViewController() self.present(child, animated: true) } }
Для простоты способ отображения будем задавать в дочернем контроллере:
class ChildViewController: UIViewController { private let transition = PanelTransition() // 1 init() { super.init(nibName: nil, bundle: nil) transitioningDelegate = transition // 2 modalPresentationStyle = .custom // 3 } … }
- Создаём объект, описывающий переход.
transitioningDelegateпомечен какweak, поэтому приходиться хранитьtransitionотдельно поstrongссылке. - Сетим наш переход в
transitioningDelegate. - Для того, чтобы управлять способом отображения в
presentationControllerнужно указывать.customдляmodalPresentationStyle..
Показываем в пол-экрана
Начнём код для PanelTransition с presentationController. Вы с ним работали, если создавали всплывающие окна через UIPopoverController. PresentationController управляет отображением контроллера: фреймом, иерархией и т.д. Он решает, как показывать поповеры на айпаде: с каким фреймом, в какую сторону от кнопки показывать, добавляет размытие в фон окна и затемнение под него.

Наша структура похожа: будем затемнять фон, ставить фрейм не в полный экран:

Для начала, в методе presentationController(forPresented:, presenting:, source:) вернём класс PresentationController:
class PanelTransition: NSObject, UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { return presentationController = PresentationController(presentedViewController: presented, presenting: presenting ?? source) }
Source – это тот контроллер, на котором мы вызвали анимацию показа. Но контроллер, который будет участвовать в транзишине — первый из иерархии, у которого установлено definesPresentationContext = true. Если контроллер сменится, то настоящий показывающий контроллер будет в параметре presenting.
Теперь можно реализовать класс PresentationController. Для начала, зададим фрейм будущему контроллеру. Для этого есть метод frameOfPresentedViewInContainerView. Пусть контроллер займёт нижнюю половину экрана:
class PresentationController: UIPresentationController { override var frameOfPresentedViewInContainerView: CGRect { let bounds = containerView!.bounds let halfHeight = bounds.height / 2 return CGRect(x: 0, y: halfHeight, width: bounds.width, height: halfHeight) } }
Можно запустить проект и попробовать показать экран, но ничего не произойдёт. Это потому, что мы теперь сами управляем иерархией вьюшек и нам надо добавить вью контроллера вручную:
// PresentationController.swift override func presentationTransitionWillBegin() { super.presentationTransitionWillBegin() containerView?.addSubview(presentedView!) }
Ещё нужно поставить фрейм для presentedView. containerViewDidLayoutSubviews – лучшее место, потому что так мы сможем реагировать и на поворот экрана:
// PresentationController.swift override func containerViewDidLayoutSubviews() { super.containerViewDidLayoutSubviews() presentedView?.frame = frameOfPresentedViewInContainerView }
Теперь можно запускать. Анимация будет стандартной для UIModalTransitionStyle.coverVertical, но фрейм будет в два раза меньше.
Затемняем фон
Следующая задача – затемнить фоновый контроллер, чтобы сфокусироваться на показанном.
Унаследуемся от PresentationController и заменим на новый класс в файле PanelTransition. В новом классе будет только код для затемнения.
class DimmPresentationController: PresentationController
Создадим вьюшку, которую будем накладывать поверх:
private lazy var dimmView: UIView = { let view = UIView() view.backgroundColor = UIColor(white: 0, alpha: 0.3) view.alpha = 0 return view }()
Будем менять alpha вьюшки согласованно с анимацией перехода. Есть 4 метода:
presentationTransitionWillBeginpresentationTransitionDidEnddismissalTransitionWillBegindismissalTransitionDidEnd
Первый из них самый сложный. Надо добавить dimmView в иерархию, проставить фрейм и запустить анимацию:
override func presentationTransitionWillBegin() { super.presentationTransitionWillBegin() containerView?.insertSubview(dimmView, at: 0) performAlongsideTransitionIfPossible { [unowned self] in self.dimmView.alpha = 1 } }
Анимация запускается с помощью вспомогательной функции:
private func performAlongsideTransitionIfPossible(_ block: @escaping () -> Void) { guard let coordinator = self.presentedViewController.transitionCoordinator else { block() return } coordinator.animate(alongsideTransition: { (_) in block() }, completion: nil) }
Фрейм для dimmView задаём в containerViewDidLayoutSubviews (как и в прошлый раз):
override func containerViewDidLayoutSubviews() { super.containerViewDidLayoutSubviews() dimmView.frame = containerView!.frame }
Анимация может быть прервана и отменена, и если отменили, то надо удалить dimmView из иерархии:
override func presentationTransitionDidEnd(_ completed: Bool) { super.presentationTransitionDidEnd(completed) if !completed { self.dimmView.removeFromSuperview() } }
Обратный процесс запускается в методах скрытия. Но теперь нужно удалять dimmView, только если анимация завершилась.
override func dismissalTransitionWillBegin() { super.dismissalTransitionWillBegin() performAlongsideTransitionIfPossible { [unowned self] in self.dimmView.alpha = 0 } } override func dismissalTransitionDidEnd(_ completed: Bool) { super.dismissalTransitionDidEnd(completed) if completed { self.dimmView.removeFromSuperview() } }
Теперь фон затемняется.
Управляем анимацией
Показываем контроллер снизу
Теперь мы можем анимировать появление контроллера. В классе PresentationController вернём класс, который будет управлять анимацией появления:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return PresentAnimation() }
Реализовать протокол просто:
extension PresentAnimation: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let animator = self.animator(using: transitionContext) animator.startAnimation() } func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { return self.animator(using: transitionContext) } }
Ключевой код чуть сложнее:
class PresentAnimation: NSObject { let duration: TimeInterval = 0.3 private func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { // transitionContext.view содержит всю нужную информацию, извлекаем её let to = transitionContext.view(forKey: .to)! let finalFrame = transitionContext.finalFrame(for: transitionContext.viewController(forKey: .to)!) // Тот самый фрейм, который мы задали в PresentationController // Смещаем контроллер за границу экрана to.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.height) let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut) { to.frame = finalFrame // Возвращаем на место, так он выезжает снизу } animator.addCompletion { (position) in // Завершаем переход, если он не был отменён transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } return animator } }
Обойти довольно просто: нужно в коде animateTransition использовать не аниматор, а старое апи UIView.animate… Например, вот так:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let to = transitionContext.view(forKey: .to)! let finalFrame = transitionContext.finalFrame(for: transitionContext.viewController(forKey: .to)!) to.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.height) UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { to.frame = finalFrame }) { (_) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } Этот метод не вызывается, если реализован `interruptibleAnimator(using transitionContext:)`
Если вы не делаете прерываемый транзишен, то метод interruptibleAnimator можно не писать. Прерываемость рассмотрим в следующей статье, подписывайтесь.
Скрываем контроллер вниз
Всё то же самое, только в обратную сторону. Класс целиком:
class DismissAnimation: NSObject { let duration: TimeInterval = 0.3 private func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { let from = transitionContext.view(forKey: .from)! let initialFrame = transitionContext.initialFrame(for: transitionContext.viewController(forKey: .from)!) let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut) { from.frame = initialFrame.offsetBy(dx: 0, dy: initialFrame.height) } animator.addCompletion { (position) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } return animator } } extension DismissAnimation: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let animator = self.animator(using: transitionContext) animator.startAnimation() } func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { return self.animator(using: transitionContext) } }
На этом месте можно поэкспериментировать со сторонами:
– снизу может появиться альтернативный сценарий;
– справа – быстрый переход по меню;
– сверху – информационное сообщение:
В следующий раз добавим интерактивное закрытие жестом, а потом сделаем его анимацию прерываемой. Если не терпится, то полный проект уже на гитхабе.
Подписывайтесь на канал Dodo Pizza Mobile.
ссылка на оригинал статьи https://habr.com/ru/company/dodopizzaio/blog/463527/

Добавить комментарий