Edited at

UIViewControllerAnimatedTransitioning によるカスタムトランジション

More than 1 year has passed since last update.

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
}

}