0

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.

Neph
  • 1,823
  • 2
  • 31
  • 69
  • The scroll view's gesture recognisers is probably interfering with your gesture recognisers. – Sweeper Mar 22 '21 at 13:43
  • @Sweeper I didn't add an extra gesture recognizer to the ScrollView and if it's got one (no idea how the ScrollView works exactly), I didn't touch it. The only recognizer I did "mess" with is the MarkerView one but that one doesn't work. – Neph Mar 22 '21 at 13:48
  • The scroll view has got a gesture recogniser. Otherwise you can't scroll it, can you? You don't need to touch it for it to interfere with your gesture recognisers. See this [possible duplicate](https://stackoverflow.com/questions/17721163/uitapgesturerecognizer-on-uilabels-in-subview-of-uiscrollview-not-working). – Sweeper Mar 22 '21 at 13:52
  • you're not using the MarkerView initializer override, as in `let marker = MarkerView.init(frame: CGRect.zero)` – CSmith Mar 22 '21 at 13:57
  • @CSmith I just put a print in `init(frame: CGRect)` and it's printed to console with my version. I also tested yours and didn't notice a difference. Is there a reason why the longer version should be used instead? – Neph Mar 22 '21 at 14:16
  • @Sweeper Hm, interesting, thanks for the link. Unfortunately that question is for ObjC, while I'm using Swift (apart from the function call), so it only hints at a possible answer but doesn't provide an actual one (so no duplicate). I tested the answer: Added `gesture.delegate = self` in `commitInit` and `UIGestureRecognizerDelegate` as implementation, plus the code in [this](https://stackoverflow.com/q/30829973/2016165) question (the Swift 5 version looks a bit different) but this new func is never called. – Neph Mar 22 '21 at 14:36
  • my comment/question was just to ensure that your `commonInit()` method was getting called and the gesture recognizer set. I assume you've also tried adding the gesture recognizer to different view (e.g. the StackView)?. – CSmith Mar 22 '21 at 15:06
  • @CSmith Yes, I tried to add it to the `contentView`/`view` and the `stackedView`. I also tried to add it to the `MarkerView` in `createMarkers` or `setUpMarkers` in the `ViewController` class but none of it worked. I've also got a label outside the scrollView and adding the gesture recognizer to that though code worked just fine (obviously without the extra delegate). Sweeper might be right that it's a problem with the ScrollView but the suggestion in the link to add the delegate didn't work either. – Neph Mar 22 '21 at 15:17
  • the suggestion in the link is about setting the delegate but also "implement `gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:` to return YES" – CSmith Mar 22 '21 at 15:46
  • @CSmith Yes, that's what I did (at least I think I found the right function), it looks a bit different because I'm using Swift, not ObjC. Please look at my reply to Sweeper. – Neph Mar 22 '21 at 15:49
  • I added the code changes according to your suggestion, @Sweeper to the question. Any idea why this isn't working? – Neph Mar 23 '21 at 09:58
  • I fixed it, see the answer below. Even though @Sweeper's link didn't provide an actual answer (the `delegate` version didn't work), one of the comments hinted at a problem. So thanks again for the link! – Neph Mar 23 '21 at 15:58

1 Answers1

0

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).
Neph
  • 1,823
  • 2
  • 31
  • 69