xyk blog

最近は iOS 開発の記事が多めです。

UITabBar の中央のタブを大きな画像ボタンに変更する2

検証環境:
Xcode 12.3
Swift 5.3.2

f:id:xyk:20201223181426p:plain

前回の記事の実装では問題が発生することが発覚したので実装方法を見直す。
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

f:id:xyk:20201228182429p:plain

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

参考

equaleyes.com