はじめに
画面のモーダル遷移を実現するUIViewController
のpresent(viewControllerToPresent: UIViewController,
animated: Bool, completion: (() -> Void)?)
や dismiss(animated: Bool, completion: (() -> Void)?)
では
completion
があり画面遷移完了後の処理を記述することができます。
一方でUINavigationController
のpushViewController(viewController: UIViewController, animated: Bool)
や
popViewController(animated: Bool)
ではcompletion
がなくデフォルトでは、
画面遷移完了後の処理を記述することができません。本記事ではUINavigationController
でもdismiss
メソッドと同様に
画面遷移完了後の処理を記述する方法を記載します。
環境設定
以下の環境を使用しています。
- Xcode10.2.1
- Swift 5.0.1
方法
UINavigationController
に以下のメソッドを追加します。
transitionCoordinator
はViewController
の遷移に関するアニメーションを規定するProtocolで、
遷移実行時にUIViewController
のtransitionCoordinator
プロパティに格納されます。
transitionCoordinator
のanimate(alongsideTransition:, completion:)
メソッドのcompletion
が
transition
の完了後に呼ばれるため、ここに実行したいメソッドを入れれば画面遷移完了後に
メソッドを呼ぶという目的を達成できます。
// TransitionCoordinatorはViewControllerのtransitionに関するアニメーションを規定するProtocolです // transitionCoordinatorはactiveなtransitionやpresentation/dismissalが実行されている時に // viewControllerのtransitionCoordinatorに含まれ、transitionが完了したタイミングで解放されます extension UINavigationController { func popViewController(animated: Bool, completion: @escaping (() -> Void)) { popViewController(animated: animated) if animated, let coordinator = transitionCoordinator { coordinator.animate(alongsideTransition: nil) { _ in // coordinatorで実行するanimationの完了時にcompletionが実行されます // 本メソッドにおいてcontextが同じanimationはpopViewControllerのため // pop完了後に本メソッドが実行されます completion() } } else { completion() } } func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping (() -> Void)) { pushViewController(viewController, animated: animated) if animated, let coordinator = transitionCoordinator { coordinator.animate(alongsideTransition: nil) { _ in // coordinatorで実行するanimationの完了時にcompletionが実行されます // 本メソッドにおいてcontextが同じanimationはpushViewControllerのため // push完了後に本メソッドが実行されます completion() } } else { completion() } } }
ログの出力
以下のメソッドでログ出力を行い呼ばれる順番を確認しました。
// 呼び出し元 viewWillDisappear viewDidDisappear viewWillAppear viewDidAppear // 呼び出し先 viewWillDisappear viewDidDisappear viewWillAppear viewDidAppear // Completion // UIViewController present および dismiss // UINavigationController push および pop
結果は以下のようになりました。Aが呼び出し元のViewController、Bが遷移先のViewControllerです。 なおanimatedはtrueであってもfalseであっても同様の結果になりました。
// UIViewControllerのpresentおよびdismiss A viewWillDisappear B viewWillAppear B viewDidAppear A viewDidDisappear present completion B viewWillDisappear A viewWillAppear A viewDidAppear B viewDidDisappear dismiss completion // 本記事でのUINavigationControllerのpushおよびpop A viewWillDisappear B viewWillAppear A viewDidDisappear B viewDidAppear push completion B viewWillDisappear A viewWillAppear B viewDidDisappear A viewDidAppear pop completion
modalでの遷移かpushでの遷移かの違いにより、viewWillAppearやviewDidAppearなどライフサイクル部分の順序に違いはありますが、
completionの呼ばれるタイミングは遷移元および遷移先のライフサイクルが全て完了した後であり、
present
メソッドやdismiss
メソッドのcompletionで実行しようとしていることをするには問題ないであろうことがわかります。
なお、補足ですが、たまにみられる以下の書き方ですと、completionのタイミングがvewDidDisappearやveiwDidAppearの前となってしまい、 presentやdismissとタイミングが大きく異なるため意図した挙動にならない可能性が高いです。
extension UINavigationController { func popViewController(animated: Bool, completion: (() -> Void)? = nil) { CATransaction.begin() CATransaction.setCompletionBlock(completion) popViewController(animated: animated) CATransaction.commit() } func pushViewController(_ viewController: UIViewController, animated: Bool, completion: (() -> Void)? = nil) { CATransaction.begin() CATransaction.setCompletionBlock(completion) pushViewController(viewController, animated: animated) CATransaction.commit() } } // ログの結果 A viewWillDisappear B viewWillAppear push completion A viewDidDisappear B viewDidAppear B viewWillDisappear A viewWillAppear pop completion B viewDidDisappear A viewDidAppear
参考
- Apple Document - TransitionCoordinator
- TransitionCoordinatorに関する説明があります
- Apple Document - UIViewControllerTransitionCoordinatorContext
- 遷移を実行中のViewControllerに関する情報を提供するContextの説明があります