【Swift】親Viewの領域外に設置したボタンを反応させる

設置したUIButtonを親Viewの外に配置してしまうとタップが反応しない。
よくあるのは✗ボタンが右上に半分はみ出しているようなカスタムViewを作ったりするとき。

f:id:rasukarusan:20200530170230p:plain
親Viewからはみ出して設置するタイプのボタン

この状態だとタップが反応するのは親Viewにかぶっている部分のみになる。

f:id:rasukarusan:20200530171701p:plain
親Viewからはみ出している領域は反応しない

これはボタンの反応領域を広げてあげると解決できる。

ボタンの反応領域を広げる

カスタムView内で下記を定義してあげればOK。

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let tapPoint = closeButton.convert(point, from: self) // ①
    if closeButton.bounds.contains(tapPoint) { // ②
        return closeButton
    }
    return super.hitTest(point, with: event)
}

タップした位置を取得して、タップ位置がボタン内かどうかを判定してあげるだけ。

① タップした位置がボタンとどれくらい離れているかを取得する

let tapPoint = closeButton.convert(point, from: self) // ①

これでタップした位置が、ボタンからどれくらい離れているかが取得できる。
selfはカスタムViewのことで、つまりボタンを設置している親View。

②タップした位置がボタンの範囲内にいるかを判定する

if closeButton.bounds.contains(tapPoint) { // ②
    return closeButton
}

タップ位置がボタンの領域内であれば、ボタンを返す。こうすることで、たとえ親Viewの領域外でも「ボタンをタップした」と認識させることができる。
つまりここでタップ領域を広げているイメージ。

全体のソース

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let customView = CustomView(frame:CGRect(x: 100, y: 100, width: 100, height: 100))
        view.addSubview(customView)
    }
}

class CustomView: UIView {
    var closeButton: UIButton!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .systemBlue
        
        closeButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
        closeButton.center = CGPoint(x: bounds.maxX, y: bounds.minY)
        closeButton.setBackgroundImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
        closeButton.tintColor = .systemPink
        closeButton.addTarget(self, action: #selector(closeAction(_:)), for: .touchUpInside)
        addSubview(closeButton)
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let tapPoint = closeButton.convert(point, from: self)
        if closeButton.bounds.contains(tapPoint) {
            return closeButton
        }
        return super.hitTest(point, with: event)
    }
    
    @objc func closeAction(_: UIButton) {
        print("CloseButton tapped!")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

参考