0

I basically want to have an invisible UIView that catches taps on it (and performs some action) but also passes through this tap event to the views behind it. I want to make it some generic class that inherits from UIView.

koen
  • 5,383
  • 7
  • 50
  • 89
  • 1
    https://stackoverflow.com/a/12355957/1801544 You can make a `PassthroughView` with just that and do what you need. – Larme Mar 04 '21 at 16:56
  • @Larme please pay attention that this 'PassthroughView' isn't what I asked. it's a view that checks all its subviews and if the UIEvent is in one of them it says that the event is in it too. I want the opposite. – Tamir Nahum Mar 04 '21 at 17:54
  • What I meant is that you can use that logic, created view and override `pointInside:withEvent:` apply your own logic inside that method to do what you need. – Larme Mar 04 '21 at 17:58

1 Answers1

0

You can propagate touch events through the responder chain:

class PassTouchView: UIView {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // do what you want here
        print("View was touched!")
        // propagate up the responder chain (most likely the superview)
        self.next?.touchesBegan(touches, with: event)
    }
}

Trying to do this with Gesture Recognizers is a bit different. If you want to do that, you probably want to use protocol/delegate pattern.


Edit

Quick example of protocol/delegate pattern when the "covering view" is using a UITapGestureRecognizer.

The protocol:

protocol PassTapDelegate {
    func passMeTheTap(_ recognizer: UITapGestureRecognizer)
}

A custom "pass the tap" view:

class PassTapView: UIView {
    
    var ptDelegate: PassTapDelegate?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        let t = UITapGestureRecognizer(target: self, action: #selector(self.gotTap(_:)))
        addGestureRecognizer(t)
    }
    
    @objc func gotTap(_ recognizer: UITapGestureRecognizer) -> Void {
        print("Got Tap in PassTap view")
        ptDelegate?.passMeTheTap(recognizer)
    }
}

A sample view controller showing how to use it:

class PassTappingViewController: UIViewController, PassTapDelegate {
    
    var btnsArray: [UIButton] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let colors: [UIColor] = [
            .systemBlue, .systemGreen, .systemOrange, .systemTeal,
        ]
        let centers: [CGPoint] = [
            CGPoint(x: -75.0, y: -75.0),
            CGPoint(x:  75.0, y: -75.0),
            CGPoint(x: -75.0, y:  75.0),
            CGPoint(x:  75.0, y:  75.0),
        ]
        
        var x: CGFloat = 0
        var y: CGFloat = 0
        var i: Int = 1
        
        for (pt, c) in zip(centers, colors) {
            let b = UIButton()
            b.backgroundColor = c
            b.setTitle("Button \(i)", for: [])
            b.translatesAutoresizingMaskIntoConstraints = false
            btnsArray.append(b)
            view.addSubview(b)

            // all buttons are 120 x 120
            b.widthAnchor.constraint(equalToConstant: 120.0).isActive = true
            b.heightAnchor.constraint(equalTo: b.widthAnchor).isActive = true

            x = pt.x
            y = pt.y
            
            // uncomment these two lines to see the tap passing through
            //  to multiple overlapping buttons
            //x = -75.0
            //y = -75.0 + CGFloat(i) * 40.0

            b.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: x).isActive = true
            b.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: y).isActive = true
            
            i += 1
        }
        
        // add a red PassTapView
        let passTapView: PassTapView = PassTapView()
        
        passTapView.translatesAutoresizingMaskIntoConstraints = false
        passTapView.backgroundColor = .systemRed
        view.addSubview(passTapView)
        
        // center it so it's covering portions of the buttons
        NSLayoutConstraint.activate([
            passTapView.widthAnchor.constraint(equalToConstant: 120.0),
            passTapView.heightAnchor.constraint(equalTo: passTapView.widthAnchor),
            passTapView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            passTapView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
        
        // add action for each button
        btnsArray.forEach { b in
            b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
        }
        
        // set self as the custom delegate for passTapView
        passTapView.ptDelegate = self
        
    }
    
    @objc func btnTapped(_ sender: Any?) -> Void {
        // unwrap the sender as a button and get its title
        guard let b = sender as? UIButton,
              let t = b.currentTitle
        else {
            return
        }
        print("Button \(t) was tapped!")
    }
    
    func passMeTheTap(_ recognizer: UITapGestureRecognizer) {
        // loop through buttons array to see if tap from
        //  passTapView is inside any of them
        btnsArray.forEach { b in
            if b.bounds.contains(recognizer.location(in: b)) {
                self.btnTapped(b)
            }
        }
    }
    
}

The code will create 4 buttons in a grid, with a PassTapView overlaid on top so it covers part of each button:

enter image description here

Tapping the Red view where it covers part of a button will "pass the tap" gesture to the delegate, which will trigger the .touchUpInside action if the tap location is contained in a button.

If we un-comment two lines in the controller (see the comments), it will lay out the buttons in a stack so they overlap each other:

enter image description here

Now, tapping the left side of the red view will trigger the .touchUpInside action for multiple buttons because the tap gesture location will fall inside more than one of them.

DonMag
  • 69,424
  • 5
  • 50
  • 86