UITabBarController において選択されたタブに応じた処理をしたい時

先日、 Google Analytics などのイベントトラッキング系の実装をしている時に、
「**選択されたタブのログを取りたい、ただし現在開いているタブは取らなくていい。**」という場面に出会いました。

画面に依らない共通の処理だったので、それぞれの `ViewController` ではなく `UITabBarController` に書くことにしました。
簡単な実装かと思いきや、結構奥深い学びがあったので共有します。

- バッドプラクティスとそれが悪い実装となる理由
- 最終的な実装例

を紹介します。

**2020 年に Swift 始めたばかりの初心者なので、アドバイス・指摘待っています！！！**

## ざっくりとした結論
始めに、最終的に至った形を示しておきます。
後の理解を深めるためと、時間がない人のためです。

1. `tabBar(_:didSelect:)` を使おう
2. 引数 `item` と `items` 配列でパターンマッチングしよう
3. `item.tag` を使った指定はバッドプラクティスになりがちなので、極力避けよう！

これを読んで、「そんなの当たり前じゃん？」ってなった方はもうここから先を読む必要はないです。
逆に「なんでそれがバッドプラクティス？」ってなった方は読んでみて下さい！！

## バッドプラクティス: `item.tag` を使う
ググってみると、ちょこちょこ見かける方法ですが、これは **基本的にバッドプラクティスになりがち** です。
具体的には以下のような実装ですね。 (僕も最初こうやってました)

```swift
class TabBarController: UITabBarController {
    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        switch item.tag {
        case 0:
            // tag = 0 に対する処理
        case 1:
            // tag = 2 に対する処理
            ...
        default:
            break
        }
    }
}
```

理由はまだしっかり理解できていませんが (誰が教えて…)、

1. 個別に `tag` を設定する必要がある
2. そしてその `tag` を参照することで依存が生まれる

からかなーと思っています。

> 個別に `tag` を設定する必要がある

ということは、別に左から 0, 1, 2, 3, ... と付けなくても良いわけで…
めちゃめちゃ屁理屈な人が 4, 8, 12, ... とか付けてたら泣きますよね笑

このように特定の `UIView` インスタンスへの依存を生むような実装は避けるべきです。

## 最終的な実装例
じゃあどうするかというと、メソッドの引数である `item` と `tabBar.items` の配列を照らし合わせる形で実装しました。

まず、実装の前提ですが、以下のような `Tab` というタブの型を定義しています。

```swift

class TabBarController: UITabBarController {
    private enum Tab: Int {
        case home
        case history
        case setting
    }
    ...
}
```

使うメソッドは先と同じく、 `tabBar(_:didSelect:)` です。
ただし異なる点として、 `tabBar.items` の配列とパターンマッチングしていきます。

```swift

class TabBarController: UITabBarController {
    ...

    // 現在選択されているタブをプロパティとして持っておく
    private var selectedTab: Tab = .home

    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        guard let firstIndex = tabBar.items?.firstIndex(of: item),
              let tab = Tab(rawValue: firstIndex)
              else { return }

        // 表示されている画面と同じタブがタップされた場合は何もしない
        if tab == selectedTab { return }

        switch tab {
        case .home:
            // .home 画面に対する処理
        case .history:
            // .history 画面に対する処理
        case .setting:
            // .setting 画面に対する処理
        }

        // 選択されているタブの更新
        selectedTab = tab
    }
}
```

肝になるのはここですね。

```swift

guard let firstIndex = tabBar.items?.firstIndex(of: item),
      let tab = Tab(rawValue: firstIndex)
      else { return }
```

何をしているかというと、

1. 引数である `item` で選択された `UITabBarItem` を取得して
2. `tabbar` の `items` (`UITabBarItem` が順番に入った入った箱) の中の何番目かを `firstIndex(of:)` で調べて
3. `Tab` 型に変換する

ということをしています。
最後の型の変換は今回の実装ならではですが、 `2` 番目までで、選択されたタブの順番が取得できるので、あとはその順番を使って処理を作っていけばいいです。

この実装では `UIKit` や `UIView` への依存を生んでおらず、 `Tab` を使って `UITabBar` をセットアップしていけば後でバグが生まれるということが少ないです。
(仮にバグが生まれても、発見が早い)

## まとめ
`UITabBarController` 初めて触ったのですが、奥深すぎて仲良くなれる気がしません…
(`UITabBarController` と `UITabBar` の違いも分かってない)

---
**追記**
本記事公開後、 @lovee さんより、`UITabBarController` と `UITabBar` の違いについてコメント頂いたのでそのまま記載します！！

> ちなみに `UITabBar` と `UITabBarController` の違いはそれぞれの名前とおり、 `UITabBar` はバー（`UIView` 継承）で `UITabBarController` はコントローラー（`UIViewController` 継承）です

---
ですが、今回の実装を経験して、「後から保守性が落ちるコード」、「クラッシュ・バグを生みかねないコード」を書かないように意識して実装したいと思いました。
いずれは人のコード読んで「なんか怪しいな」と怪しいコードを **嗅ぎ分けられる** ようになりたいです

この記事は自分の備忘録を建前として、 iOS エンジニアの皆様から「もっとこうしたら良いよ！」 「こんな方法もあるよ！」 「その実装だとこんな時危険だよ！！」 という意見を頂戴することを裏の目的としていますので、
**アドバイス・指摘バンバンください！**

### 参考
- [selectedItem, Apple](https://developer.apple.com/documentation/uikit/uitabbar/1623453-selecteditem)
- [tabBar(_:didSelect:), Apple](https://developer.apple.com/documentation/uikit/uitabbardelegate/1623463-tabbar)