先日、 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 エンジニアの皆様から「もっとこうしたら良いよ!」 「こんな方法もあるよ!」 「その実装だとこんな時危険だよ!!」 という意見を頂戴することを裏の目的としていますので、
アドバイス・指摘バンバンください!