0

I have found lots of similar questions about not receiving touch events and I understand that in some cases, writing a custom hitTest function may be required - but I also read that the responder chain will traverse views and viewControllers that are in the hierarchy - and I don't understand why a custom hitTest would be required for my implementation.

I'm looking for an explanation and/or a link to a document that explains how to test the responder chain. This problem is occurring in Xcode 10.2.1.

My scenario (I am not using Storyboard):

  • I have a mainViewController, that provides a full screen view with an ImageView and a few Labels. I have attached TapGestureRecognizers to the ImageView and one of the labels - and they both work properly.
  • When I tap the label, I add a child viewController and it's view as a subview to the mainViewController. The view is constrained to cover only the right-half of the screen.
  • The child viewController contains a vertical stack view that contains 3 arrangedSubviews.
  • Each arrangedSubview contains a Label and a horizontal StackView.
  • The horizontal stackView's each contain a View with a Label as a subview.
  • The Label in the subview sets it's isUserInteractionEnabled flag to True and adds a TapGestureRecognizer.
  • These are the only objects in the child ViewController that have 'isUserInteractionEnabled' set.

The Label's are nested fairly deep, but since this is otherwise a direct parent/child hierarchy (as opposed to the 2 views belonging to a NavigationController), I would expect the Label's to be in the normal responder chain and function properly. Do the Stack View's change that behavior? Do I need to explicitly set the 'isUserInteractionEnabled' value to False on some of the views? Is there way I can add logging to the ResponderChain so I can see which views it checked and find out where it is being blocked?

After reading this StackOverflow post I tried adding my gesture recognizers in viewDidLayoutSubviews() instead of what's shown below - but they still do not receive tap events.

Thank you in advance to any who can offer advice or help.

Here is the code for the label that is not responding to my tap events and the tap event it should call:

    func makeColorItem(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UIView {
    let colorNumber:Int = colorLabelDict.count
    let colorView:UIView = {
        let v = UIView()
        v.tag = 700 + colorNumber
        v.backgroundColor = .clear
        v.contentMode = .center
        return v
    }()
    self.view.addSubview(colorView)
    let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
    let colorChoice: UILabel = {
        let l = UILabel()
        l.tag = 700 + colorNumber
        l.isUserInteractionEnabled = true
        l.addGestureRecognizer(tapColorGR)
        l.text = colorName
        l.textAlignment = .center
        l.textColor = fgColor
        l.backgroundColor = bgColor
        l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
        l.layer.borderColor = fgColor.cgColor
        l.layer.borderWidth = 1
        l.layer.cornerRadius = 20
        l.layer.masksToBounds = true
        l.adjustsFontSizeToFitWidth = true
        l.translatesAutoresizingMaskIntoConstraints = false
        l.widthAnchor.constraint(equalToConstant: 100)
        return l
    }()
    colorView.addSubview(colorChoice)
    colorChoice.centerXAnchor.constraint(equalTo: colorView.centerXAnchor).isActive = true
    colorChoice.centerYAnchor.constraint(equalTo: colorView.centerYAnchor).isActive = true
    colorChoice.heightAnchor.constraint(equalToConstant: 50).isActive = true
    colorChoice.widthAnchor.constraint(equalToConstant: 100).isActive = true
    colorLabelDict[colorNumber] = colorChoice
    return colorView
}

    @objc func tapColor(sender:UITapGestureRecognizer) {
    print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
    if let cn = sender.view?.tag {
        colorNumber = cn
        let v = colorLabelDict[cn]
        if let l = (v?.subviews.first as? UILabel) {
            print("The \(l.text) label was tapped.")
        }
    }
}
Jim
  • 55
  • 1
  • 7
  • It would help if you provide a bit more code - enough to make it clear what's going on. It *looks* like you are adding `UILabel` references to `colorLabelDict` ... in this line: `colorLabelDict[colorNumber] = colorChoice` your `colorChoice` object is a `UILabel`, but in your `tapColor` func you are trying to get the first subview? – DonMag Sep 09 '20 at 16:18
  • You're right. That's leftover code from when I tried to add the tapGestureRecognizer to the View instead of the label. In that case, the sender.view was the UIView and the Label was it's first subview. Thanks for pointing that out. I'll correct it once I am finally able to get the tap event to fire. – Jim Sep 09 '20 at 18:16

1 Answers1

1

It looks like the main reason you're not getting a tap recognized is because you are adding a UILabel as a subview of a UIView, but you're not giving that UIView any constraints. So the view ends up with a width and height of Zero, and the label exists outside the bounds of the view.

Without seeing all of your code, it doesn't look like you need the extra view holding the label.

Take a look at this... it will add a vertical stack view to the main view - centered X and Y - and add "colorChoice" labels to the stack view:

class TestViewController: UIViewController {
    
    let stack: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.spacing = 4
        return v
    }()
    
    var colorLabelDict: [Int: UIView] = [:]
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v1 = makeColorLabel(colorName: "red", bgColor: .red, fgColor: .white)
        let v2 = makeColorLabel(colorName: "green", bgColor: .green, fgColor: .black)
        let v3 = makeColorLabel(colorName: "blue", bgColor: .blue, fgColor: .white)

        [v1, v2, v3].forEach {
            stack.addArrangedSubview($0)
        }

        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)
        
        NSLayoutConstraint.activate([
            stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }
    
    func makeColorLabel(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UILabel {
        let colorNumber:Int = colorLabelDict.count
        // create tap gesture recognizer
        let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
        let colorChoice: UILabel = {
            let l = UILabel()
            l.tag = 700 + colorNumber
            l.addGestureRecognizer(tapColorGR)
            l.text = colorName
            l.textAlignment = .center
            l.textColor = fgColor
            l.backgroundColor = bgColor
            l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
            l.layer.borderColor = fgColor.cgColor
            l.layer.borderWidth = 1
            l.layer.cornerRadius = 20
            l.layer.masksToBounds = true
            l.adjustsFontSizeToFitWidth = true
            l.translatesAutoresizingMaskIntoConstraints = false
            // default .isUserInteractionEnabled for UILabel is false, so enable it
            l.isUserInteractionEnabled = true
            return l
        }()
        NSLayoutConstraint.activate([
            // label height: 50, width: 100
            colorChoice.heightAnchor.constraint(equalToConstant: 50),
            colorChoice.widthAnchor.constraint(equalToConstant: 100),
        ])
        // assign reference to this label in colorLabelDict dictionary
        colorLabelDict[colorNumber] = colorChoice
        // return newly created label
        return colorChoice
    }
    
    @objc func tapColor(sender:UITapGestureRecognizer) {
        print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
        // unwrap the view that was tapped, make sure it's a UILabel
        guard let tappedView = sender.view as? UILabel else {
            return
        }
        let cn = tappedView.tag
        let colorNumber = cn
        print("The \(tappedView.text ?? "No text") label was tapped.")
    }
}

Result of running that:

enter image description here

Those are 3 UILabels, and tapping each will trigger the tapColor() func, printing this to the debug console:

A Color was tapped...with tag:700
The red label was tapped.
A Color was tapped...with tag:701
The green label was tapped.
A Color was tapped...with tag:702
The blue label was tapped.
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Originally, I tried adding just the labels to the stack view, but the stack view manipulated their sizes (with .fillEqually). That's why I added them to the UIView and constrained them within it - figuring that the stackView could manipulate the UIView size as much as it wanted to and my labels would still display correctly. – Jim Sep 09 '20 at 18:14
  • @Jim - well, it depends on what you're trying to do. In general, either give the arranged subviews size constraints and let them change the size of the stack view, or give the stack view constraints and let it manage the size of the arranged subviews. Without seeing your actual goal, difficult to say which route you should take. – DonMag Sep 09 '20 at 18:17
  • I have 7 color labels and want them displayed with equal sizing on a single horizontal line. They should look very similar to the output you provided - but horizontally and equally spaced across the view they are constrained to. I admit - I've just started using StackViews and don't understand them fully. It's usually a lot of trial and error with the distribution value to finally get it to look right. Let me try to just add a frame to my container UIView (so it's non-zero) and see if that makes it work. – Jim Sep 09 '20 at 18:25
  • @Jim - in the code you posted, you're setting each label width to `100` ... that's obviously not going to fit on half of a screen. Show an image of what you're really trying to do. – DonMag Sep 09 '20 at 18:27
  • the app is designed (currently anyway) to run on iPad only, landscape only, and I generalized when I said 1/2 screen - it's actually about 2/3 - so it does fit. I'm going to accept your answer as correct because I appreciate your time and it *DID* help me correct the problem. It was like you stated - The textView holding the label had a width of 0. I added a widthConstraint to it and it's working now. Thank you very much. – Jim Sep 09 '20 at 20:04