Search code examples
iosswiftuiviewuigesturerecognizeruitapgesturerecognizer

UITapGestureRecognizer in custom UIView doesn't work


Swift 5/Xcode 12.4

I created an xib file for my custom MarkerView - the layout's pretty simple:

- View
-- StackView
--- DotLabel
--- NameLabel

View and StackView are both set to "User Interaction Enabled" in the inspector by default. DotLabel and NameLabel aren't but ticking their boxes doesn't seem to actually change anything.

At runtime I create MarkerViews (for testing purposes only one atm) in my ViewController and add them to a ScrollView that already contains an image (this works):

override func viewDidAppear(_ animated: Bool) {
    createMarkers()
    setUpMarkers()
}

private func createMarkers() {
    let marker = MarkerView()
    marker.setUp("Some Text")
    markers.append(marker)
    scrollView.addSubview(marker)
    marker.alpha = 0.0
}

private func setUpMarkers() {
    for (i,m) in markers.enumerated() {
        m.frame.origin = CGPoint(x: (i+1)*100,y: (i+1)*100)
        m.alpha = 1.0
    }
}

These MarkerViews should be clickable, so I added a UITapGestureRecognizer but the linked function is never called. This is my full MarkerView:

class MarkerView: UIView {
    @IBOutlet weak var stackView: UIStackView!
    @IBOutlet weak var dotLabel: UILabel!
    @IBOutlet weak var nameLabel: UILabel!
    
    let nibName = "MarkerView"
    var contentView:UIView?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        self.addSubview(view)
        contentView = view
        
        let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
        self.addGestureRecognizer(gesture)
    }
    func loadViewFromNib() -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nibName, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
    
    func setUp(_ name:String) {
        nameLabel.text = name
        nameLabel.sizeToFit()
        stackView.sizeToFit()
    }
    
    @objc private func clickedMarker() {
        print("Clicked Marker!")
    }
}

Other questions recommend adding self.isUserInteractionEnabled = true before the gesture recognizer is added. I even enabled it for the StackView and both labels and also tried to add the gesture recognizer to the ScrollView but none of it helped.

Why is the gesture recognizer not working and how do I fix the problem?

Edit: With Sweeper's suggestion my code now looks like this:

class MarkerView: UIView, UIGestureRecognizerDelegate {
    .....
    func commonInit() {
        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
        gesture.delegate = self
        self.isUserInteractionEnabled = true
        self.addGestureRecognizer(gesture)
        self.addSubview(view)
        contentView = view
    }
    .....
    func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        print("gestureRecognizer")
        return true
    }
}

"gestureRecognizer" is never printed to console.


Solution

  • One of the comments below the accepted answer for the question @Sweeper linked gives a good hint:

    What is the size of the content view (log it)?

    print(self.frame.size)
    print(contentView.frame.size)
    

    both printed (0.0, 0.0) in my app. The UITapGestureRecognizer is attached to self, so even if it worked, there was simply no area that you could tap in for it to recognize the tap.

    Solution: Set the size of the UIView the UITapGestureRecognizer is attached to:

    stackView.layoutIfNeeded()
    stackView.sizeToFit()
    self.layoutIfNeeded()
    self.sizeToFit()
    

    didn't work for me, the size was still (0.0, 0.0) afterwards. Instead I set the size of self directly:

    self.frame.size = stackView.frame.size
    

    This also sets the size of the contentView (because it's the MarkerView's child) but of course requires the size of stackView to be set properly too. You could add up the childrens' widths/heights (including spacing/margins) yourself or simply call:

    stackView.layoutIfNeeded()
    stackView.sizeToFit()
    

    The finished code:

    func setUp(_ name:String) {
        nameLabel.text = name
        //nameLabel.sizeToFit() //Not needed anymore
        stackView.layoutIfNeeded() //Makes sure that the labels already use the proper size after updating the text
        stackView.sizeToFit()
        self.frame.size = stackView.frame.size
    
        let gesture = UITapGestureRecognizer(target: self, action: #selector(self.onClickStackView))
        self.addGestureRecognizer(gesture)
    }
    

    Specifics:

    1. There's no need for a delegate and overriding func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool to always return true, which might be an alternative but I didn't manage to get that version to work - the function was never called. If someone knows how to do it, please feel free to post it as an alternative solution.
    2. Attaching the UITapGestureRecognizer has to be done once the parent view knows its size, which it doesn't in commonInit because the text of the labels and the size of everything isn't set there yet. It's possible to add more text after the recognizer is already attached but everything that exceeds the original length (at the time the recognizer was attached) won't be clickable if using the above code.
    3. The parent view's background color is used (the stackView's is ignored) but it's possible to simply set it to a transparent color.
    4. Attaching the gesture recognizer to stackView instead of self works the same way. You can even use a UILabel but their "User Interaction Enabled" is off by default - enable it first, either in the "Attributes Inspector" (tick box in the "View" section) or in code (myLabel.isUserInteractionEnabled = true).