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

  • 12
    Like
  • 2
    Comment

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
    }

}