UINavigationController とモーダルの場合とで実装方法が異なるのでメモ。
UINavigationController のトランジション(プッシュ/ポップ遷移)をカスタマイズ
方針:
- UIViewControllerAnimatedTransitioning に準拠したアニメーターオブジェクトを実装
- UINavigationControllerDelegate を実装してアニメーターオブジェクトを返却
-
pushViewController(_:animated:)
またはpopViewControllerAnimated(_:)
を実施
アニメーターオブジェクト
UIViewControllerAnimatedTransitioning に準拠したアニメーターを実装する。一般的には以下のように実装する。
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.35
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 遷移元ビューコントローラー
let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
// 遷移先ビューコントローラー
let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
// トランジションコンテクストからアニメーションを描画するためのコンテナービューを取得
let containerView = transitionContext.containerView
// トランジションコンテクストのコンテナービューに to.view を乗せる
containerView.insertSubview(to.view, belowSubview: from.view)
// コンテナービュー上でアニメーションを描画。transition でも良い
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: UIViewAnimationOptions.init(rawValue: 0),
animations: {
from.view.alpha = 0.0
},
completion: { (finished: Bool) in
from.view.alpha = 1.0
// トランジションが完了したことを通知
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
これを更に Push / Pop を考慮するように修正。
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
var navigationOperation: UINavigationControllerOperation = .none
convenience init(_ navigationOperation: UINavigationControllerOperation) {
self.init()
self.navigationOperation = navigationOperation
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.35
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
customAnimation(transitionContext)
}
private func customAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let containerView = transitionContext.containerView
containerView.insertSubview(to.view, belowSubview: from.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: UIViewAnimationOptions.init(rawValue: 0),
animations: {
from.view.alpha = 0.0
},
completion: { _ in
from.view.alpha = 1.0
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
アニメーションに UIView.transition() を利用する場合
例えば customAnimation()
の中身をこのようにすれば良い。以下の例では左右にフリップする。
private func customAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
let containerView = transitionContext.containerView
let options: UIViewAnimationOptions = (navigationOperation == UINavigationControllerOperation.pop) ? [.transitionFlipFromLeft] : [.transitionFlipFromRight]
containerView.insertSubview(to.view, belowSubview: from.view)
UIView.transition(from: from.view,
to: to.view,
duration: transitionDuration(using: transitionContext),
options: options,
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
UINavigationControllerDelegate
UINavigationControllerDelegate に準拠したアニメーションコントローラーを実装する。ここで適当なアニメーターを返す。
UINavigationControllerDelegate
// Push: From -> To / Pop: From <- To それぞれのペアを定義する
private var classPairs: [(from: AnyClass, to: AnyClass)] {
return [
(from: MasterViewController.self, to: DetailViewController.self),
(from: DetailViewController.self, to: MasterViewController.self),
]
}
private func isTargetViewControllerPair(from: UIViewController, to: UIViewController) -> Bool {
for pair in classPairs {
if from.classForCoder == pair.from && to.classForCoder == pair.to {
return true
}
}
return false
}
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationControllerOperation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if isTargetViewControllerPair(from: fromVC, to: toVC) {
// 遷移元/先が対象ビューコントローラーであれば、適当なアニメーターを返す
return Animator(operation)
}
return nil
}
Storyboard の UINavigationController にアニメーションコントローラーのインスタンスを配置して、デリゲートの接続を行えば良い。
遷移処理を実施
pushViewController(_:animated:)
または popViewControllerAnimated(_:)
を実施するとアニメーターで実装したアニメーションが適用される。Storyboard Segue による遷移でも同様のはず。
モーダルビューのトランジションをカスタマイズ
UIViewControllerTransitioningDelegate でアニメーターを返すデリゲートメソッドを実装する。
extension HogeViewController: UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Animator()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Animator()
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return InteractiveAnimator() // UIViewControllerInteractiveTransitioning
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return InteractiveAnimator() // UIViewControllerInteractiveTransitioning
}
}