【UIKit】UITabBarControllerとUINavigationControllerを組合わせた場合のNavigation Item操作方法

# はじめに
`UITabBarController`と`UINavigationController`を組合わせた場合の`Navigation Item`の操作方法にハマったので、整理してみました

- `Navigation` -> `TabBar` -> `Navigation` -> `ViewController`
- `Navigation` -> `TabBar` -> `ViewController`

上記2パターンで実装した時の`Navigation Item`はどのように指定できるのか確認しました

# 実装

UINavigationController : `Navigation`
UITabBarController : `TabBar`
と表現します


## `Navigation` -> `TabBar` -> `Navigation` -> `ViewController`
Menu画面
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/28f12423-7105-490f-f9a1-f1a1603d6ca3.png">
TabBar画面
![スクリーンショット 2022-06-20 14.23.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/fd0ca5aa-b942-0dac-1054-1b080239aac1.png)

Menu画面 -> TabBar画面　の処理

```swift: Menu画面
final class MenuViewController: UIViewController {
    @objc
    func gameButtonSelected() {
        let storyboard = UIStoryboard(name: "Game", bundle: nil)
        let tabBarCon = storyboard.instantiateViewController(identifier: "Game")
        self.navigationController?.pushViewController(tabBarCon, animated: true)
        self.navigationController?.setNavigationBarHidden(true, animated: true)

        // ViewControllerを取得したいなら
        // guard let tabBar = tabBarCon as? UITabBarController else { return }
        // guard let navBarCon = tabBar.viewControllers?[0] as? UINavigationController else { return }
        // guard let destinationVC = navBarCon.topViewController as? ___ViewController else { return }
    }
}
```

このままではNavigationBarが二重で表示されてしまうので、
最初の`Navigation` -> `TabBar`の遷移で`self.navigationController?.setNavigationBarHidden(true, animated: true)`としました

`Tab Bar Item`はタブごとの`navigation`で(storyboard上で)定義しています

`Navigation Item`についても各ViewControllerで定義したものが表示されます

```swift: 各ViewController
final class GameViewSettingViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        tabBarController?.navigationItem.title = R.string.localizable.gameViewSetting()
        navigationItem.hidesBackButton = true
        navigationItem.leftBarButtonItem = self.createBarButtonItem(image: UIImage.named("questionmark.circle"), select: #selector(self.questionBarButtonItem))
    }
    
    private func createBarButtonItem(image: UIImage, select: Selector) -> UIBarButtonItem {
        let button = UIButton(type: .custom)
        button.setImage(image, for: .normal)
        button.addTarget(self, action: safeSelect, for: .touchUpInside)
        let barButtonItem = UIBarButtonItem(customView: button)
        return barButtonItem
    }
```

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/08b6144c-78c4-49f3-fa9f-4097545840c9.png">

#### TabBar画面の`ViewController`からさらに画面遷移させる
`TabBar`の`ViewController`から`self.navigationController?.pushViewController`で画面遷移させたところ遷移先画面のナビゲーションバーが表示されませんでした

そこで遷移時の処理に`self.navigationController?.setNavigationBarHidden(false, animated: true)`を追加したところ無事表示されました

```swift: TabBar画面のViewController
final class GameViewSettingViewController: UIViewController {
    @objc
    func questionBarButtonItem() {
        let storyboard = UIStoryboard(name: "HowToUseDetail", bundle: nil)
        let vc = storyboard.instantiateViewController(identifier: "HowToUseDetail")
        self.navigationController?.pushViewController(vc, animated: true)
        self.navigationController?.setNavigationBarHidden(false, animated: true)
    }
}
```

が、今度は遷移元の画面へ戻った時にナビゲーションバーが2重で表示されてしまいました

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/6f58c3c5-5ada-74e2-fbe6-aaaa7cd4cecd.png">

遷移元へ戻る時に`navigationController?.setNavigationBarHidden(true, animated: true)`を追加したところ期待した表示になりました

```swift: 遷移先のViewController
    @objc
    func back() {
        navigationController?.popViewController(animated: true)
        navigationController?.setNavigationBarHidden(true, animated: true)
    }
```

しかし、期待通りの動きにはなりましたが、都度`.setNavigationBarHidden`をさせて無理矢理表示をコントロールしている点にはとても違和感を感じるため、`TabBar`以降の`Navigation`を抜いての実装も試してみました

## `Navigation` -> `TabBar` -> `ViewController`

Menu画面からのTabBar画面の遷移処理部分を修正します（`TabBar`はコード実装へ変更）

```swift: Menu画面
final class MenuViewController: UIViewController {
    @objc
    func gameButtonSelected() {
        let tabBarCon = UITabBarController()
        let gamePlayerVC = R.storyboard.gameDPlayer.instantiateInitialViewController()!
        let gameDataVC = R.storyboard.gameData.instantiateInitialViewController()!
        let gameResultVC = R.storyboard.gameResult.instantiateInitialViewController()!
        tabBarCon.setViewControllers([gamePlayerVC, gameDataVC, gameResultVC], animated: true)
        self.navigationController?.setNavigationBarHidden(false, animated: true)
        self.navigationController?.pushViewController(tabBarCon, animated: true)
    }
}
```
`TabBar`後の`Navigation`を消したことにより
`Tab Bar Item`は各ViewControllerで定義したものが表示されますが、`Navigation Item`が反映されなくなりました

ここは要注意箇所なようで、`TabBar`のnavigationItemに定義してあげる必要があるようです

また、実際のUIを考えると、各タブごとに`Navigation Item`は変えるべき（少なくともタイトルは変わる？）であり
以下のような`tabBarController?.navigationItem`を修正する処理を各ViewControllerで行う必要があります
- `tabBarController?.navigationItem.title = `
- `tabBarController?.navigationItem.hidesBackButton =`
- `tabBarController?.navigationItem.leftBarButtonItem =`

ただし、
この処理をViewDidLoadでやるとタブ遷移後に戻った場合は二度目が呼ばれないので、
ViewWillAppearで処理する必要があります

```swift: 各ViewController
final class GameViewSettingViewController: UIViewController {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        tabBarController?.navigationItem.title = R.string.localizable.gameViewSetting()
        tabBarController?.navigationItem.hidesBackButton = true
        tabBarController?.navigationItem.leftBarButtonItem = self.createBarButtonItem(image: UIImage.named("questionmark.circle"), select: #selector(self.questionBarButtonItem))
    }
}
```

（もし`Tab Bar Item`のタイトルを変える場合は、storyboard上だけでなく
`tabBarController?.tabBarItem.title = `でもできます）

#### TabBar画面の`ViewController`からさらに画面遷移させる
`self.navigationController?.pushViewController`が使えなくなり
`self.tabBarController?.parent as? UINavigationController`で一度`Navigation`を取得してから`.pushViewController`するようにします

`self.navigationController?.setNavigationBarHidden`の処理を考えなくて済むのが良いところです

```swift: TabBar画面のViewController
final class GameViewSettingViewController: UIViewController {
    @objc
    func questionBarButtonItem() {
        let storyboard = UIStoryboard(name: "HowToUseDetail", bundle: nil)
        let vc = storyboard.instantiateViewController(identifier: "HowToUseDetail")
        let navigationCon = self.tabBarController?.parent as? UINavigationController
        navigationCon?.pushViewController(vc, animated: true)
    }
}
```

# おわりに
この記事を書いている途中に見かけましたが、もしかするとUITabBarControllerを途中に表示するのはよくないのかな？？
アプリの使い勝手としては、ぜひこのまま使いたいところですが、アンチパターンなら作り変える必要もあるような。

`UIViewController`, `UINavigationController`, `UITabBarController`のメソッドをもっと調べればよりよい実装ができる気もしているので、引き続き改善を目指して行こうと思います。
（そもそも`UITabBarController`使わないで実装できたりするかも？と思ったり）


# 参考

https://teratail.com/questions/291659

https://ja.stackoverflow.com/questions/4464/navigationcontroller%E3%81%ABtabbarcontroller%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

https://hiromiick.com/swift-nav-tab-by-code/