LoginSignup
35
29

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-01-25

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
    }

}
35
29
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
29