Edited at

UINavigationControllerの遷移が完了したタイミングで任意の処理を実行する

More than 1 year has passed since last update.

UINavigationControllerを使った遷移で、完了したタイミングを取りたいケースってあるかと思います。いくつかできそうな方法をまとめてみました。


遷移先のViewControllerのライフサイクルの中に書く

単純にviewWillAppear(_:)やviewDidAppear(_:)を使ってしまうと、戻ってくる時との区別がつかないので困ります。ただし、UINavigationControllerはコンテナ型のViewControllerなので、遷移先ViewControllerが渡された時に、willMoveToParentViewController()やdidMoveToParentViewController()を呼んでいる模様です。そこでisMovingToParentViewControllerを使えば、自前でフラグ管理しなくてもNavigation Stackに積まれた時のみに処理を実行できます。

https://developer.apple.com/reference/uikit/uiviewcontroller/2097561-ismovingtoparentviewcontroller

    override func viewWillAppear(_ animated: Bool) {

super.viewWillAppear(animated)
if isMovingToParentViewController {
// NavigationControllerで表示されようとしている時に実行する内容
}
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if isMovingToParentViewController {
// NavigationControllerで表示された時に実行する内容
}
}


Navigation Stackを管理しているUINavigationControllerのデリゲートメソッド内に書く

UINavigationControllerにもViewControllerが追加される度に呼ぶデリゲートメソッドがあるので、これを使うこともできます。

https://developer.apple.com/reference/uikit/uinavigationcontrollerdelegate

    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {

// ViewControllerが表示されようとしている時に実行したい処理
}

func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// ViewControllerが表示された時に実行したい処理
}

しかし、例えば対象のアプリが複雑な構成をしていて、仕事で一画面だけ特殊な遷移を実装するような場合は、UINavigationControllerのdelegateに何か入れるのは抵抗があるでしょう。また、既にどこかで一括管理されてしまっている場合、その中で特定のViewControllerに対するif文を追加しなければならないなどコードが混沌としていきます。

因みに、先述した方法ではUINavigationControllerのrootViewControllerとして指定したViewControllerに対してisMovingToParentViewControllerの値がtrueになることはありません。navigationController(_:willShow:animated:)や

navigationController(_:didShow:animated:)の場合はrootViewControllerに対しても呼ばれるようです。


遷移アニメーション完了コールバック内に書く

他のやり方としては、遷移の完了はアニメーションの終了だと捉え、CoreAnimationの中の完了処理として記述する方法もあります。

CATransactionのsetCompletionBlockへ完了処理を書くのが簡単です。UINavigationControllerの遷移も結局Core Animationのアニメーションなので、通常の完了コールバックを実装するときと同じように、pushViewController(_:animated:)をCATransactionで囲みます。すると、遷移アニメーションが完了すると、コールバックの中の処理が実行されます。

https://developer.apple.com/reference/quartzcore/catransaction

// 遷移先のViewController

let detailVC = DetailViewController(nibName: nil, bundle: nil)

// CATransactionを開始
CATransaction.begin()
// ViewControllerへの遷移をbegin(), commit()で囲む
navigationController?.pushViewController(detailVC, animated: true)
// アニメーションの完了処理を実装
CATransaction.setCompletionBlock {
// 遷移完了後の処理をここへ書く
}
// CATransactionをコミット
CATransaction.commit()

ただしこの方法には罠があって、DetailViewControllerのviewDidLoad(:)やviewDidAppear(:)にアニメーション処理があると、setCompletionBlockが呼ばれません。(例えばUIAcitivityIndicatorViewが置かれているときなど)

そこで、CATransitionを作ってCAAnimationDelegateのイベントを拾う方法を取ります。このやり方によって、UINavigationControllerの特定の遷移アニメーションに対するDelegateを拾うことができます。

https://developer.apple.com/reference/quartzcore/catransition

// 遷移先のViewController

let detailVC = DetailViewController(nibName: nil, bundle: nil)

// CATransitionでそれっぽい遷移アニメーションを作成
let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight

// CAAnimationDelegateのイベントを取得する
transition.delegate = self

// アニメーションをUINavigationControllerへ設定
navigationController?.view.layer.add(transition, forKey: nil)
// デフォルトの遷移アニメーションをオフにして画面遷移を行なう
navigationController?.pushViewController(detailVC, animated: false)

これで、アニメーションの開始・終了をとれます。

func animationDidStart(_ anim: CAAnimation) {

// 遷移アニメーションの開始時に呼ばれる
}

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
// 遷移アニメーションの完了時に呼ばれる
}

外から遷移の完了タイミングをコントロールしつつ、他のViewControllerへの影響範囲を気にできるという点で便利です。ただし、pushViewController(detailVC, animated: false)でfalseを設定している事によって、アニメーションは完了していないにもかかわらずviewDidAppearが呼ばれてしまうため、想定外のことが起きる危うさはあります。

本気でアニメーションによる制御をやりたいのであれば、UIViewControllerAnimatedTransitioningを使ったカスタム遷移を実装するのがいいかもしれません(そうすると結局isMovingToParentViewControllerを使う話と同じになってくるかもしれんせんが)。

以上です。なんとなくこれだって言う方法を決め兼ねていたので整理してみました。