先日、 Google Analytics などのイベントトラッキング系の実装をしている時に、
「選択されたタブのログを取りたい、ただし現在開いているタブは取らなくていい。」という場面に出会いました。
画面に依らない共通の処理だったので、それぞれの ViewController ではなく UITabBarController に書くことにしました。
簡単な実装かと思いきや、結構奥深い学びがあったので共有します。
- バッドプラクティスとそれが悪い実装となる理由
- 最終的な実装例
を紹介します。
2020 年に Swift 始めたばかりの初心者なので、アドバイス・指摘待っています!!!
ざっくりとした結論
始めに、最終的に至った形を示しておきます。
後の理解を深めるためと、時間がない人のためです。
-
tabBar(_:didSelect:)を使おう - 引数
itemとitems配列でパターンマッチングしよう -
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
}
}
}
理由はまだしっかり理解できていませんが (誰が教えて…)、
- 個別に
tagを設定する必要がある - そしてその
tagを参照することで依存が生まれる
からかなーと思っています。
個別に
tagを設定する必要がある
ということは、別に左から 0, 1, 2, 3, ... と付けなくても良いわけで…
めちゃめちゃ屁理屈な人が 4, 8, 12, ... とか付けてたら泣きますよね笑
このように特定の UIView インスタンスへの依存を生むような実装は避けるべきです。
最終的な実装例
じゃあどうするかというと、メソッドの引数である item と tabBar.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 }
何をしているかというと、
- 引数である
itemで選択されたUITabBarItemを取得して -
tabbarのitems(UITabBarItemが順番に入った入った箱) の中の何番目かをfirstIndex(of:)で調べて -
Tab型に変換する
ということをしています。
最後の型の変換は今回の実装ならではですが、 2 番目までで、選択されたタブの順番が取得できるので、あとはその順番を使って処理を作っていけばいいです。
この実装では UIKit や UIView への依存を生んでおらず、 Tab を使って UITabBar をセットアップしていけば後でバグが生まれるということが少ないです。
(仮にバグが生まれても、発見が早い)
まとめ
UITabBarController 初めて触ったのですが、奥深すぎて仲良くなれる気がしません…
(UITabBarController と UITabBar の違いも分かってない)
追記
本記事公開後、 @lovee さんより、UITabBarController と UITabBar の違いについてコメント頂いたのでそのまま記載します!!
ちなみに
UITabBarとUITabBarControllerの違いはそれぞれの名前とおり、UITabBarはバー(UIView継承)でUITabBarControllerはコントローラー(UIViewController継承)です
ですが、今回の実装を経験して、「後から保守性が落ちるコード」、「クラッシュ・バグを生みかねないコード」を書かないように意識して実装したいと思いました。
いずれは人のコード読んで「なんか怪しいな」と怪しいコードを 嗅ぎ分けられる ようになりたいです
この記事は自分の備忘録を建前として、 iOS エンジニアの皆様から「もっとこうしたら良いよ!」 「こんな方法もあるよ!」 「その実装だとこんな時危険だよ!!」 という意見を頂戴することを裏の目的としていますので、
アドバイス・指摘バンバンください!