UITabBarとUITabBarControllerについて調べた
概要
大阪オフィスの山田です。最近、湿気がすごくてかなしいです。 今更ながら、UITabBarとUITabBarControllerの使い方を調べました。アプリに何度も登場せず、一度作ると作ったっきりになりがちなので改めて調べてみよう、という動機です。なお、公式のドキュメントはこちらです。
開発環境
- macOS: 10.15.4
- Xcode: 11.5
- 検証端末: iPhone SE 2nd Generation(Simulator)
目次
少し長いので、目次を記載しておきます。
- Interface Builderを使って、TabBarControllerを追加する
- TabBarの設定を変更する
- TabBarItemの設定を変更する
- プログラムからTabBarControllerを生成する
- タブアイテムタップ時にモーダルで画面を表示する
- タブバーの高さを変更する
- バッジを付ける、消す
- タブ切り替え時のアニメーション
- 参考
- おわりに
Interface Builderを使って、TabBarControllerを追加する
StoryboardでLibaryを開いて、TabBarControllerを追加します。
TabBarControllerを追加すると、付随してViewControllerが2つ追加されました。
TabBarControllerをInitial Launch ViewControllerに設定し、ViewControllerを次の画像のように、背景色設定とラベルを配置しました。
TabBarの設定を変更する
TabBarをタップし、Background
とShadow
に画像を設定しました。
Shadow
はTabBarの上端部分に表示されるようです。
Selection
に画像を設定すると、選択しているタブのアイコン画像の後ろに、画像が表示されます。
Style
ではblackを選ぶことができ、TabBarが黒くなります。
Translucent
は、TabBarの透過/非透過を設定することができます。
Bar Tint
で、TabBarの色を自由に変更することができます。
Item Positioning
で、Barの項目の配置を変更することが可能です。
Item Positioning
はAutomatic
、Fill
、Centered
から選択できます。
Fill
は、TabBarに広がるように項目を配置し、必要に応じて項目の間隔を調整します。Centered
は、項目の幅と間隔を設定して、項目を中央から配置します。
Automatic
は、環境に応じて(compact, regular)Fill
、Centered
が自動で設定されます。
Centered
に設定した時の動作がこちら。
TabBarItemの設定を変更する
IB上で、追加されたViewControllerの下部にTab Bar Itemが表示されているので、選択してプロパティを見ていきます。Badge
にはバッジとして表示する文字を設定することができます。
バッジはタブを選択しただけでは自動で消えません。またバッジの色も変更することができます。
アイコンとタイトルはSystemのものを選択することができます。
アイコンとタイトルを独自のものにするには、Custom
を選択して、Selected Image
、Title
、Image
を設定します。
横向きにした時に表示する画像をLandscape
で指定することができます。画像ではわかりにくいですがPencil
を指定しています。
プログラムからTabBarControllerを生成する
IBを使用しないで、プログラムだけでTabBarControllerを作成します。 まず、子となるViewControllerを定義します。
class FirstViewController: UIViewController { lazy var centerLabel: UILabel = { let label = UILabel() label.text = "First" label.font = UIFont.boldSystemFont(ofSize: 70.0) label.textColor = UIColor.white return label }() override func loadView() { view = UIView() view.backgroundColor = .blue centerLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(centerLabel) NSLayoutConstraint.activate([ centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) } }
class SecondViewController: UIViewController { lazy var centerLabel: UILabel = { let label = UILabel() label.text = "Second" label.font = UIFont.boldSystemFont(ofSize: 70.0) label.textColor = UIColor.white return label }() override func loadView() { view = UIView() view.backgroundColor = .brown centerLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(centerLabel) NSLayoutConstraint.activate([ centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) } }
次にTabBarControllerを定義します。
class MainTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() setupTab() } } private extension MainTabBarController { func setupTab() { let firstViewController = FirstViewController() firstViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .history, tag: 0) let secondViewController = SecondViewController() secondViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0) viewControllers = [firstViewController, secondViewController] } }
それぞれ、子のViewControllerを生成し、tabBarItem
プロパティを設定し、viewControllers
プロパティに子のViewControllerを設定します。この記事の目的から少し外れますが、SceneDelegateも修正を加えます。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let scene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: scene) self.window = window let vc = MainTabBarController() window.rootViewController = vc }
これで、起動時に定義したMainTabBarController
が表示され、タブで表示するViewControllerを切り替えることができました。
タブアイテムタップ時にモーダルで画面を表示する
モーダルで表示する画面のクラスを定義します。
class ModalViewController: UIViewController { lazy var centerLabel: UILabel = { let label = UILabel() label.text = "Modal" label.font = UIFont.boldSystemFont(ofSize: 70.0) label.textColor = UIColor.white return label }() override func loadView() { view = UIView() view.backgroundColor = .darkGray centerLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(centerLabel) NSLayoutConstraint.activate([ centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) } }
TabBarControllerに以下の処理を追加します。
- TabBarControllerのdelegateを設定
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
メソッドを実装
class MainTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() setupTab() } } private extension MainTabBarController { func setupTab() { delegate = self // delegateを設定 let firstViewController = FirstViewController() firstViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .history, tag: 0) let secondViewController = SecondViewController() secondViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0) viewControllers = [firstViewController, secondViewController] } } extension MainTabBarController: UITabBarControllerDelegate { // shouldSelectメソッドを実装 func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { if viewController == tabBarController.viewControllers?[1] { present(ModalViewController(), animated: true, completion: nil) return false } return true } }
shouldSelectのメソッドで、タブアイテムをタップした時のイベントを拾います。今回は2つ目のタブアイテムをタップした時に、モーダル表示する処理を入れています。falseを返却することでActiveなタブをそのままにしておくことができます。そのため、2つ目のタブをタップしても、選択されているタブは変更されません。
プログラムからアクティブなタブを設定した時の挙動
UITabBarControllerのselectedIndex
プロパティに値をセットすることで、アクティブなタブをプログラムから設定することができます。この場合、先ほど実装したshouldSelect
メソッドはコールされないため、モーダルで画面は表示されず、タブが切り替わる挙動になります。以下、検証用の実装です。
class FirstViewController: UIViewController { lazy var centerLabel: UILabel = { let label = UILabel() label.text = "First" label.font = UIFont.boldSystemFont(ofSize: 70.0) label.textColor = UIColor.white return label }() // 「2つ目のタブを選択」ボタンを追加 lazy var selectSecondTabButton: UIButton = { let button = UIButton() button.setTitle("2つ目のタブを選択", for: .normal) return button }() override func loadView() { view = UIView() view.backgroundColor = .blue centerLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(centerLabel) NSLayoutConstraint.activate([ centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) // 「2つ目のタブを選択」ボタンを配置し、タップ時のイベントを設定 selectSecondTabButton.translatesAutoresizingMaskIntoConstraints = false selectSecondTabButton.addTarget(self, action: #selector(tapSelectSecondTabButton), for: .touchUpInside) view.addSubview(selectSecondTabButton) NSLayoutConstraint.activate([ selectSecondTabButton.topAnchor.constraint(equalTo: centerLabel.bottomAnchor), selectSecondTabButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) } } private extension FirstViewController { // 「2つ目のタブを選択」ボタンをタップしたら、2つ目のタブをアクティブにする @objc func tapSelectSecondTabButton(_ sender: UIButton) { self.tabBarController?.selectedIndex = 1 } }
この例では以下の処理を追加しています。
- 子のViewControllerにボタンを追加
- ボタンがタップされたら、tabBarControllerプロパティにてUITabBarControllerにアクセスして、selectedIndexを変更
モーダルダイアログを表示しつつ、後ろのTabBarのアクティブタブを変更してみる
結論から言うと可能でした。以下のように実装して、TabBarControllerのselectedIndex
を変更するコードを書いて動かしたところ、正常にアクティブタブを変更できました。
※3つ目のタブを追加していますが、他のタブとほぼ一緒なので割愛します。
- Modalで表示する画面にボタンとTabBarのクロージャプロパティを追加
- TabBarControllerで、モーダル表示するタイミングで、クロージャプロパティに値を設定
class ModalViewController: UIViewController { lazy var centerLabel: UILabel = { let label = UILabel() label.text = "Modal" label.font = UIFont.boldSystemFont(ofSize: 70.0) label.textColor = UIColor.white return label }() // 「3つ目のタブを選択」ボタンを追加 lazy var selectThirdTabButton: UIButton = { let button = UIButton() button.setTitle("3つ目のタブを選択", for: .normal) return button }() lazy var closeButton: UIButton = { let button = UIButton() button.setTitle("閉じる", for: .normal) return button }() var selectThirdTab: (() -> Void)? override func loadView() { view = UIView() view.backgroundColor = .darkGray centerLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(centerLabel) NSLayoutConstraint.activate([ centerLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), centerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) // 「3つ目のタブを選択」ボタンを配置し、タップ時のイベントを設定 selectThirdTabButton.translatesAutoresizingMaskIntoConstraints = false selectThirdTabButton.addTarget(self, action: #selector(tapSelectThirdTabButton), for: .touchUpInside) view.addSubview(selectThirdTabButton) NSLayoutConstraint.activate([ selectThirdTabButton.topAnchor.constraint(equalTo: centerLabel.bottomAnchor), selectThirdTabButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) // 「閉じる」ボタンを配置し、タップ時のイベントを設定 closeButton.translatesAutoresizingMaskIntoConstraints = false closeButton.addTarget(self, action: #selector(tapCloseButton), for: .touchUpInside) view.addSubview(closeButton) NSLayoutConstraint.activate([ closeButton.topAnchor.constraint(equalTo: selectThirdTabButton.bottomAnchor), closeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) } } private extension ModalViewController { // 「3つ目のタブを選択」ボタンをタップしたら、3つ目のタブをアクティブにする @objc func tapSelectThirdTabButton(_ sender: UIButton) { selectThirdTab?() } @objc func tapCloseButton(_ sender: UIButton) { dismiss(animated: true, completion: nil) } }
extension MainTabBarController: UITabBarControllerDelegate { // shouldSelectメソッドを実装 func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { if viewController == tabBarController.viewControllers?[1] { let modal = ModalViewController() modal.selectThirdTab = { [weak self] in self?.selectedIndex = 2 } present(modal, animated: true, completion: nil) return false } return true } }
タブバーの高さを変更する
UITabBarのsizeThatFits
メソッドにて高さを設定します。TabBarをextensionするか、TabBarを継承したクラスを作って適用します。
class CustomTabBar: UITabBar { override func sizeThatFits(_ size: CGSize) -> CGSize { super.sizeThatFits(size) var sizeThatFits = super.sizeThatFits(size) sizeThatFits.height = 70 sizeThatFits.height += safeAreaInsets.bottom return sizeThatFits } }
UITabBarControllerのtabBarでCustomTabBarを使うように設定します。IBであれば、IB上のTabBarのclassにCustomTabBarクラスを指定したら良いです。プログラムのみの場合、以下のようなコードで実現しました。
class MainTabBarController: UITabBarController { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) object_setClass(tabBar, CustomTabBar.self) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupTab() } }
バッジを付ける、消す
バッジを付ける
3つ目のViewControllerにボタンを追加し、タップすると1つ目のタブにバッジを付けます。 今回、バッジの色も指定しています。
private extension ThirdViewController { // 「バッジを表示」ボタンをタップしたら、1つ目のタブにバッジを表示する @objc func tapShowBadgeButton(_ sender: UIButton) { let tabBarItem = tabBarController?.viewControllers?[0].tabBarItem tabBarItem?.badgeValue = "test" tabBarItem?.badgeColor = UIColor.purple } }
バッジを消す
1つ目のタブをタップすると、バッジを消すようにします。実装はTabControllerのshouldSelect
メソッドに記述します。UITabBarItemのbadgeValue
にnilをセットするとバッジが消えます。
// shouldSelectメソッドを実装 func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { if viewController == tabBarController.viewControllers?[0] { // badgeを消す viewController.tabBarItem.badgeValue = nil } return true }
タブ切り替え時のアニメーション
ViewControllerのアニメーション
いくつか方法はありますが、今回はUIView.transition
メソッドを使います。実装はTabControllerのshouldSelect
メソッドに記述します。
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { // ViewControllerの切り替えアニメーション guard let fromView = selectedViewController?.view, let toView = viewController.view else { return false } if fromView != toView { UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: nil) } return true }
タブアイコンのタップ時アニメーション
タブをタップした時に、アイコンがアニメーションするように実装してみます。それらしいプロパティは存在しないので以下の手順で実装を進めます。 1. タブアイコンのUIImageViewを取得する 2. タップ時にUIImageViewに対してアニメーションを実行する
タブアイコンのUIImageViewを取得する
プロパティはありませんのでsubviewsの中からUIImageViewを探し出すことにします。 Hierarchy Viewを見ると、以下のようになっています。iOS13で確認しています。
以下のように並んでいますので、一番左のUITabBarButtonを取得するにはsubviews[1]となります。Index+1をすると良さそうです。ただし、iOSのバージョンによってこの構造が違うようなので、各バージョンで確認する必要があります。
- UIBackground
- UITabBarButton
- UITabBarButton
- UITabBarButton
UITabBarButtonの中にUITabBarSwappableImageViewが存在するのでUIImageViewとして取得します。 まず、subviewsの中から再起的にsubviewsを取得して、一次元配列として扱えるようにします。 こちらの記事を参考にしました。
class CustomTabBar: UITabBar { /// indexを受け取り、タブのUIImageViewを返却する func barItemImage(index: Int) -> UIImageView? { let view = subviews[index + 1] return view.recursiveSubviews.compactMap { $0 as? UIImageView } .first } } extension UIView { // 再起的にsubviewsを取得 var recursiveSubviews: [UIView] { return subviews + subviews.flatMap { $0.recursiveSubviews } } }
タップ時にアニメーションを実行する
UITabBarControllerに戻り、didSelect
メソッドとアニメーションを実装します。
didSelect
メソッドでタップされたタブのUIImageViewを取得し、アニメーションのメソッドに渡しています。
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { guard let index = tabBar.items?.firstIndex(of: item), let customTabBar = tabBar as? CustomTabBar, let imageView = customTabBar.barItemImage(index: index) else { return } iconBounceAnimation(view: imageView) } func iconBounceAnimation(view: UIView) { UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: { view.transform = CGAffineTransform(scaleX: 1.25, y: 1.25) UIView.animate(withDuration: 0.5, delay: 0.2, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: { view.transform = CGAffineTransform(scaleX: 1, y: 1) }, completion: nil) }, completion: nil) }
参考
- UITabBar|Apple Developer Documentation
- Creating UI Elements Programmatically Using PureLayout
- Building a UIKit user interface programmatically
- UITabBarItem Icon Animation: stack overflow
- Swiftで再帰的なサブビューの取得とUI層のユニットテストへの応用: Qiita
今回のブログを書くにあたって、実装したソースコードはGitHubに上げています。
おわりに
「あれ、これ簡単にできないじゃん」ってあるあるだと思うんですよ。