ttlog

日々の開発で得た知見の技術メモ。モバイルアプリネタが多いです。

PushでPop(風アニメーション)する

UINavigationControllerを使用したPush遷移の際のアニメーションをPop風(左から右へ)にする必要があったので、実現方法についてメモしておきます。

検証環境

  • Xcode9.2
  • iOS11 Simulator

大まかな流れ

  1. Animatorの作成(UIViewControllerAnimatedTransitioningプロトコルの実装)
  2. UINavigationControllerDelegateプロトコルを実装し、1.で作成したAnimatorオブジェクトを返す
  3. Swipe Backの有効化

Animatorの作成

UIViewControllerAnimatedTransitioningプロトコルを実装したAnimatorオブジェクトを作成します。

今回は以下の2メソッドを実装しました。

  • transitionDuration(using:)

    アニメーションの実行時間を返します。

  • animateTransition(using:)

    このメソッド内に実際のアニメーション処理を実装します。引数のtransitionContextにViewController等の遷移に必要な情報が渡されます。

    1. transitionContextから遷移元、遷移先のViewを取得
    2. transitionContext.containerViewに遷移先(to)のViewを追加(addSubview)
    3. アニメーション処理(UIViewアニメーションやCoreAnimationなど)
    4. アニメーション終了を通知(transitionContext.completeTransition(true))
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.viewController(forKey: .from)?.view,
            let toView = transitionContext.viewController(forKey: .to)?.view else
        {
            transitionContext.completeTransition(true)
            return
        }
        
        transitionContext.containerView.addSubview(toView)
        
        toView.frame = CGRect(x: fromView.frame.origin.x - fromView.frame.size.width,
                              y: fromView.frame.origin.y,
                              width: toView.frame.width,
                              height: toView.frame.height)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 0,
                       options: UIViewAnimationOptions.init(rawValue: 0),
                       animations: {
                        fromView.frame = CGRect(x: fromView.frame.origin.x + fromView.frame.size.width,
                                                y: fromView.frame.origin.y,
                                                width: fromView.frame.size.width,
                                                height: fromView.frame.size.height)
                        toView.frame = CGRect(x: toView.frame.origin.x + fromView.frame.size.width,
                                              y: toView.frame.origin.y,
                                              width: toView.frame.size.width,
                                              height: toView.frame.size.height)
        },
                       completion: { _ in
                        transitionContext.completeTransition(true)
        })
    }
}

UINavigationControllerDelegateの実装

以下のメソッドを実装します。

  • navigationController(_:animationControllerFor:from:to:)

このメソッドから先ほど作成したAnimatorを返すことで、遷移アニメーションが切り替わります。

画面遷移の度に呼び出されてしまいますが、operationをチェックすることでPushによる遷移なのか、Popによる遷移なのかを判定出来ます。今回はPushのアニメーションのみを変更したいため、.pushの場合のみAnimatorを返し、その他の場合はnilを返してデフォルトのアニメーションになるようにしています。

class PopNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }

}

extension PopNavigationController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationControllerOperation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
    {
        if operation == .push {
            return PopAnimator()
        }
        return nil  // nilを返した場合、デフォルトにアニメーションになる
    }
}

f:id:kurozu10344:20171215023447g:plain

Swipe Backの有効化

UINavigationControllerDelegatenavigationController(_:animationControllerFor:from:to:)を実装すると、デフォルトのSwipe Backが無効化されてしまうようです(たとえnilを返した場合でも)。どうやらScreen Edge Swipeのイベントが発生しなくなる模様。

無効化して欲しくない場合、回避策としてUINavigationControllerinteractivePopGestureRecognizer.delegateに独自のUIGestureRecognizerDelegate実装クラスをセットし、gestureRecognizerShouldBeginメソッドでtrueを返すようにするとScreen Edge Swipeイベントが発生するようになり、Swipe Backが再び行えるようになりました。こんな対処で良いのかなという感じですが、今のところ問題は出ていません。特定の画面では無効化したい場合、適宜falseを返せば無効化されます。

class PopNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
        interactivePopGestureRecognizer?.delegate = self
    }

}

extension PopNavigationController: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return true // falseを返した場合、Swipe Backは発生しない
    }
}

サンプルコード

GitHubにサンプルコードを置いています。

PushLikePop