【iOS】UINavigationControllerでcompletionHandlerを伴ったpush/pop遷移ができるようにする

はじめに

画面のモーダル遷移を実現するUIViewControllerpresent(viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?)dismiss(animated: Bool, completion: (() -> Void)?) では completionがあり画面遷移完了後の処理を記述することができます。

一方でUINavigationControllerpushViewController(viewController: UIViewController, animated: Bool)popViewController(animated: Bool)ではcompletionがなくデフォルトでは、 画面遷移完了後の処理を記述することができません。本記事ではUINavigationControllerでもdismissメソッドと同様に 画面遷移完了後の処理を記述する方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

方法

UINavigationControllerに以下のメソッドを追加します。

transitionCoordinatorViewControllerの遷移に関するアニメーションを規定するProtocolで、 遷移実行時にUIViewControllertransitionCoordinatorプロパティに格納されます。

transitionCoordinatoranimate(alongsideTransition:, completion:)メソッドのcompletiontransitionの完了後に呼ばれるため、ここに実行したいメソッドを入れれば画面遷移完了後に メソッドを呼ぶという目的を達成できます。

// 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

参考