ViewControllerの戻るボタンのタップをハンドリングする。(戻る動作をキャンセル可能)

iOSにおいて前の画面に戻る際, 「本当に前の画面に戻りますか?」 というアラートダイアログを出して,選択次第で戻る動作をキャンセルしたい需要が生まれました。あまり大げさな実装はしたくないので,iOSが用意するUINavigationControllerによる画面遷移の仕組みに乗りたいとします。 どう対処すれば良いでしょうか。

qiita.com

こちらで紹介されている方法では,戻る動作を検知する以上のことはできませんが,これから提案する方法は戻る動作自体を取りやめることが可能になります。

なお,デメリットとして UINavigationController を拡張し,それを利用すると言うお約束が必要です。今回紹介する実装はpopする場合にとどまっていますが,modalとして呼び出された際のdismissも同じ理屈でハンドリング可能です。

UINavigationControllerの独自実装

まずは,前提となる独自実装です。UINavigationControllerへViewControllerをpushする際に,pop時のコールバックを明示的に指定するところがキモで,ここで本当にpopしていいかを一度delegateに問い合わせるということをしています。delegateが実装されていない場合は通常どおりpopされるだけです。

@objc protocol NavigationViewControllerDelegate: class {
    @objc optional func navigationController(_ navigationController: MyNavigationController,
                                             willPopViewController: UIViewController) -> Bool
}

final class MyNavigationController: UINavigationController {

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        let action = #selector(pop)
        viewController.navigationItem.leftBarButtonItem?.action = action

        super.pushViewController(viewController, animated: animated)
    }

    @objc private func pop() {
        guard let willBePoppedVC = viewControllers.last else {
            return
        }

        if let delegate = willBePoppedVC as? NavigationViewControllerDelegate {
            let willBePoppedVC = delegate.navigationController?(self, willPopViewController: willBePoppedVC) ?? true
            if !shouldPop {
                return
            }
        }

        popViewController(animated: true)
    }
}

ハンドリングしたいViewControllerの実装

navigationController(_:willDismissViewController) の返り値のtrue/falseでpopしていいかを MyNavigationController に伝えます。 たとえば shouldShowAttention というbool値を用意しておき,これがfalseのときはダイアログを出さずにそのままpop,trueのときは,一旦popを取りやめて,ダイアログの選択次第であとから明示的にpopするということが可能になります。

final class SampleViewController: UIViewController, NavigationViewControllerDelegate {
    private var shouldShowAttention = false

    // NavigationViewControllerDelegate
    func navigationController(_ navigationController: MyNavigationController,
                              willPopViewController: UIViewController) -> Bool {
        if shouldShowAttention {
            showAttentionDialogBeforeLeaving(okCallback: {[weak self] _ in
                _ = self?.navigationController?.popViewController(animated: true)
            })
        }
        
        return !shouldShowAttention
    }

    private func showAttentionDialogBeforeLeaving(okCallback: @escaping () -> Void) {
        let alertDialog = UIAlertController(title: "警告", message: "本当に戻る?", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "はい", style: .default, handler: { _ in okCallback() })
        let cancelAction = UIAlertAction(title: "いいえ", style: .cancel, handler: nil)
        
        alertDialog.addAction(cancelAction)
        alertDialog.addAction(okAction)
        
        present(alertDialog, animated: true, completion: nil)
    }
}

使い方

普通のUINavigationControllerと一緒です

let navigationVC = MyNavigationController(rootViewController: SampleViewController())
navigationVC.pushViewController(SampleViewController(), animated: false)
show(navigationVC, sender: nil)

以上,ViewControllerの戻る動作を取りやめたい場合があったので,とりあえず思いついた実装を紹介しました。意外と需要があるはずなので,何かのお役に立てれば幸いです。

追記:
バックジェスチャ?っう頭が…