PushでPop(風アニメーション)する
UINavigationControllerを使用したPush遷移の際のアニメーションをPop風(左から右へ)にする必要があったので、実現方法についてメモしておきます。
検証環境
- Xcode9.2
- iOS11 Simulator
大まかな流れ
- Animatorの作成(
UIViewControllerAnimatedTransitioning
プロトコルの実装) UINavigationControllerDelegate
プロトコルを実装し、1.で作成したAnimatorオブジェクトを返す- Swipe Backの有効化
Animatorの作成
UIViewControllerAnimatedTransitioning
プロトコルを実装したAnimatorオブジェクトを作成します。
今回は以下の2メソッドを実装しました。
transitionDuration(using:)
アニメーションの実行時間を返します。
animateTransition(using:)
このメソッド内に実際のアニメーション処理を実装します。引数の
transitionContext
にViewController等の遷移に必要な情報が渡されます。-
transitionContext
から遷移元、遷移先のViewを取得 -
transitionContext.containerView
に遷移先(to)のViewを追加(addSubview) - アニメーション処理(UIViewアニメーションやCoreAnimationなど)
- アニメーション終了を通知(
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を返した場合、デフォルトにアニメーションになる } }
Swipe Backの有効化
UINavigationControllerDelegate
のnavigationController(_:animationControllerFor:from:to:)
を実装すると、デフォルトのSwipe Backが無効化されてしまうようです(たとえnilを返した場合でも)。どうやらScreen Edge Swipeのイベントが発生しなくなる模様。
無効化して欲しくない場合、回避策としてUINavigationController
のinteractivePopGestureRecognizer.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にサンプルコードを置いています。