UITabBar の中央のタブを大きな画像ボタンに変更する2
検証環境:
Xcode 12.3
Swift 5.3.2
前回の記事の実装では問題が発生することが発覚したので実装方法を見直す。
xyk.hatenablog.com
問題というのは、UINavigationController のプッシュで画面遷移する時に、事前に遷移先 UIViewController の hidesBottomBarWhenPushed
を true にしておくとTabBar を非表示にできる機能があるのだが、このときに中央ボタンが Tabbar のサブビューではないため、いっしょに消えず画面に残ってしまう。
修正案1
前回のコードから中央ボタンの貼り付け先をUITabBarController.view
ではなくUITabBarController.tabbar
に変更した。
import UIKit class MyTabBarController: UITabBarController { let middleButton = UIButton(type: .custom) let middleButtonHeight: CGFloat = 100 override func viewDidLoad() { super.viewDidLoad() setupMiddleButton() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() tabBar.bringSubviewToFront(middleButton) } private func setupMiddleButton() { middleButton.addTarget(self, action: #selector(handleMiddleButton), for: .touchUpInside) middleButton.setImage(UIImage.init(named: "play"), for: .normal) middleButton.translatesAutoresizingMaskIntoConstraints = false tabBar.addSubview(middleButton) middleButton.widthAnchor.constraint(equalToConstant: middleButtonHeight).isActive = true middleButton.heightAnchor.constraint(equalToConstant: middleButtonHeight).isActive = true middleButton.centerXAnchor.constraint(equalTo: tabBar.centerXAnchor).isActive = true let heightDifference = (tabBar.frame.height / 2) - (middleButtonHeight / 2) middleButton.topAnchor.constraint(equalTo: tabBar.topAnchor, constant: heightDifference).isActive = true } @objc func handleMiddleButton(_ sender: UIButton) { let controller = UIViewController() controller.view.backgroundColor = .white present(controller, animated: true) } } extension MyTabBarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { // 選択されたタブが中央の場合は画面遷移が発生しないように false を返す。 // ここでは中央のタブの viewController に ViewController2 を設定していたとする。 // ちなみに tabBarController.selectedIndex はまだ遷移前の index が入っているので判定には使えない。 if viewController is ViewController2 { return false } return true } func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { print("didSelect: \(tabBarController.selectedIndex)") } }
これでhidesBottomBarWhenPushed
が true の場合には、TabBar といっしょに非表示になった。
しかし TabBar に貼り付けたので、中央ボタンが親の TabBar の範囲内に収まっている場合は問題ないが、TabBar より中央ボタンのサイズが大きく、はみ出る場合には、はみ出た部分のタップは反応しなくなる。
修正案2
TabBar からはみ出た部分もタップが反応するようにしたい。
UITabBar を継承したサブクラスを作成する。
その中でpoint(inside:with:)
をオーバーライドし、UITabBar の範囲外でも中央ボタン上のタップ座標である場合は true を返すようする。
これで hitTest が TabBar で止まらず、サブビューの中央ボタンまで呼び出されるようになり、ボタンに addTarget で追加したイベントが実行されるようになる。
import UIKit class MyTabBar: UITabBar { var didTapHandler: (() -> Void)? private let middleButtonHeight: CGFloat = 100 private let middleButton = UIButton() override func awakeFromNib() { super.awakeFromNib() setupMiddleButton() } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let convertedPoint = middleButton.convert(point, from: self) // 中央ボタンが円の場合 if UIBezierPath(ovalIn: middleButton.bounds).contains(convertedPoint) { return true } // 中央ボタンが矩形の場合 /* if middleButton.bounds.contains(convertedPoint) { return true } */ return super.point(inside: point, with: event) } func setupMiddleButton() { middleButton.setImage(UIImage.init(named: "play"), for: .normal) middleButton.addTarget(self, action: #selector(handleMiddleButton), for: .touchUpInside) addSubview(middleButton) middleButton.translatesAutoresizingMaskIntoConstraints = false middleButton.widthAnchor.constraint(equalToConstant: middleButtonHeight).isActive = true middleButton.heightAnchor.constraint(equalToConstant: middleButtonHeight).isActive = true middleButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true let heightDifference = (frame.height / 2) - (middleButtonHeight / 2) middleButton.topAnchor.constraint(equalTo: topAnchor, constant: heightDifference).isActive = true } @objc func handleMiddleButton() { didTapHandler?() } }
この MyTabBar を StoryBoard 上で TabBar のクラスとして設定しておく。
そして UITabBarController を継承したサブクラスで中央ボタンのタップイベントをハンドリングする。
class MyTabBarController: UITabBarController { var didTapCenterButton: (() -> Void)? override func viewDidLoad() { super.viewDidLoad() let myTabBar = tabBar as! MyTabBar myTabBar.didTapHandler = { [weak self] in let controller = UIViewController() controller.view.backgroundColor = .white self?.present(controller, animated: true) } delegate = self } } extension MyTabBarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { if viewController is ViewController2 { return false } return true } func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { print("didSelect: \(tabBarController.selectedIndex)") } }