[Из песочницы] История одного вью-контроллера, который хотел показываться красиво
Жил был скромный вью-контроллер 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)
}
И выглядело это не то чтобы плохо, но скучно и обыденно:
Но была у нашего героя мечта научиться показываться и скрываться по-красоте. Да так, чтобы можно было эту красоту менять потом по праздникам или просто в честь хорошего настроения.
Шли года… и так и осталась бы мечта мечтой, если бы не узнал 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)
})
}
- Вытащить презентуемый вью-контроллер (в нашем случае VCYellow) и сфоткать его. Фотка нужна для упрощения анимации.
- Получить вьюшку, на которой будет происходить анимационное колдунство. Назовем её контекст.
- Нацепить вьюху конечного контроллера на контекст и скрыть её. Показать
- её было решено после того как закончится анимация.
- Подготовить фотку для анимации. Уменьшить до начальных размеров и кинуть на контекст.
- Расщеперить фотку на весь экран, тем самым анимировав процесс презентации.
- После окончания анимации показать настоящую вьюху конечного контроллера,
- избавиться от фотки и сообщить, что действо окончено.
В результате вышел вот такой аниматор для показа:
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 презентовать себя и о чудо!
Магия сработала! Мечта VCYellow сбылась! Теперь он может показываться и скрываться как ему захочется и ничто не будет ограничивать его фантазию!
Проект-пример можно скачать тут
Статья, которую я использовал для вдохновения находится тут