UINavigationControllerの戻るイベントを取得したい！

![Xcode-12.0](https://img.shields.io/badge/Xcode-12.0-brightgreen)  ![iOS-14.0](https://img.shields.io/badge/iOS-14.0-brightgreen)

## はじめに
`UINavigationController` の左上に表示される「戻る」ボタンのイベントを取得したいことがたまにあります！

「戻る」ボタンのイベントを取得するために試行錯誤した結果です。

結論から言うとちゃんとイベントを取得したいなら `navigationItem.leftBarButtonItem` をカスタムしましょう:frowning2:

## サンプル
サンプルとして `FirstiViewController`（赤） -> `SecondViewController`（青） -> `ThirdViewController`（緑） と push で遷移する画面構成で `SecondViewController` の「戻る」について考えます。

<img width="500" alt="screens" src="https://user-images.githubusercontent.com/34936885/100622386-24ad1080-3364-11eb-8e7e-1e1f9a812059.png">

`SecondViewController` からは Full Screen の modal 遷移（黄）もつけています。

### deinit を使う
`SecondViewController` から戻る際は `deinit` が呼ばれるのでここで試してみます。

```swift
deinit {
    print("戻る！！！")
}
```

`SecondViewController` から `FirstViewController` に戻る際に呼ばれますがここだと `ThirdViewController` から `popToRootViewController` などで　`FirstViewController`　に戻った場合と区別することが困難です:no_good:

## UINavigationControllerDelegate を使う
`UINavigationController` には `UINavigationControllerDelegate` がありこのデリゲートを使ってイベント取得を試みます。

`SecondViewController` から戻った場合は `FirstiViewController` に戻るのでデリゲートで表示されるのが `FirstiViewController` かどうかで下記のように判定します。

```swift
extension SecondViewController: UINavigationControllerDelegate {

    // ナビゲーション遷移の直前に呼ばれる
    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        if viewController is FirstViewController {
            print("戻る！！！")
        }
    }

    // ナビゲーションの遷移直後に呼ばれる
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        if viewController is FirstViewController {
            print("戻る！！！")
        }
    }
}
```

`willShow` の方は `ThirdViewController` から `popToRootViewController` などで　`FirstViewController`　に戻った際も呼ばれてしまうので使うなら `didShow` の方になるでしょう。

「戻る」押下時にログ表示とかならできますがアラートを表示したいなど遷移を止めたいときなどは残念ながらこれではダメです:no_good:

（あと個人的に `UINavigationControllerDelegate` を各画面で使いたくない:no_good:）

## viewDidDisappear を使う
`viewWillDisappear` と `viewDidDisappear` を使って試してみます。

こちらは `willShow`, `didShow` と同じようなタイミングで呼ばれます（`SecondViewController` が非表示になるタイミングです）。

気をつけないといけないのは modal 遷移の Full Screen などでも呼ばれることです。

`SecondViewController` から `ThirdViewController` に遷移する場合と modal 遷移の Full Screen などで他の画面に遷移する場合を判定しないといけないので下記のようにします。

```swift
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if navigationController?.viewControllers.contains(self) == false {
        print("戻る")
    }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    if navigationController == nil {
        print("戻る")
    }
}
```

`didShow` と同じで「戻る」押下時にログ表示とかならできますがアラートを表示したいなど遷移を止めたいときなどは残念ながらこれではダメです:no_good:

## leftBarButtonItem をカスタムする
上に書いたやつはすべて厳密に言うと「戻る」ボタンを押下したかどうかは取れていません:no_good:
スワイプで戻るでも呼ばれるし、別にボタンを置いて `popViewController` しても呼ばれてしまいます。それに遷移途中でイベントを取っているので遷移自体を止めることはできません（アラート出してキャンセルとかできない）。

結局のところ `leftBarButtonItem` をカスタムしてボタンの押下イベントを取るしかありません！！

```swift
override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton(type: .system)
    button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
    button.setTitle("Back", for: .normal)
    button.setImage(UIImage(named: "back"), for: .normal)
    button.titleLabel?.font = UIFont.systemFont(ofSize: 16)
    button.imageEdgeInsets = .init(top: 0, left: -8, bottom: 0, right: 0)
    navigationItem.leftBarButtonItem = .init(customView: button)
}

@objc private func back(_ sender: Any) {
    print("戻る！！！")
    navigationController?.popViewController(animated: true)
}
```

見た目も限りなく戻るボタン:sunglasses:

<img width="112" alt="back_img" src="https://user-images.githubusercontent.com/34936885/100622472-3e4e5800-3364-11eb-9b87-7ad27834e6ea.png">

使った画像はこれです（てきとーに作りました）。

![back](https://user-images.githubusercontent.com/34936885/100622445-38587700-3364-11eb-8431-e707e004f958.png)

これで「戻る」ボタン押下イベントを完璧に取得できました:tada:  
がしかし、 `leftBarButtonItem` をカスタムするとスワイプバックできないし iOS14 からできたロングタップのメニュー表示もできなくなります:expressionless:

## 結論
厳密に「戻る」ボタン押下イベントを取得したいなら `leftBarButtonItem` のカスタムかなという感じです。  
（わたしの場合は戻るときにログとりたいという要件だったので `viewDidDisappear` を使いました。）

## おわりに
「戻る」ボタン押下イベントの取得方法としては結局、 `leftBarButtonItem` のカスタムに落ち着きましたがそもそも戻るときにアラート出したいとかの場合は本当に push でいいのか遷移方法を見直すべきな気もします:no_good: