LoginSignup
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-27

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

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

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

を紹介します。

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

ざっくりとした結論

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

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

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

バッドプラクティス: item.tag を使う

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

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 インスタンスへの依存を生むような実装は避けるべきです。

最終的な実装例

じゃあどうするかというと、メソッドの引数である itemtabBar.items の配列を照らし合わせる形で実装しました。

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


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

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


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

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


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

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

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

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

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

まとめ

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


追記
本記事公開後、 @lovee さんより、UITabBarControllerUITabBar の違いについてコメント頂いたのでそのまま記載します!!

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


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

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

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4