简体   繁体   中英

UILabel not clickable in stack view programmatically created Swift

My question and code is based on this answer to one of my previous questions. I have programmatically created stackview where several labels are stored and I'm trying to make these labels clickable. I tried two different solutions:

  1. Make clickable label. I created function and assigned it to the label in the gesture recognizer:

     public func setTapListener(_ label: UILabel){ let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:))) tapGesture.numberOfTapsRequired = 1 tapGesture.numberOfTouchesRequired = 1 label.isUserInteractionEnabled = true label.addGestureRecognizer(tapGesture) } @objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) { print(gesture.view?.tag) }

but it does not work. Then below the second way....

  1. I thought that maybe the 1st way does not work because the labels are in UIStackView so I decided to assign click listener to the stack view and then determine on which view we clicked. At first I assigned to each of labels in the stackview tag and listened to clicks:

     let tap = UITapGestureRecognizer(target: self, action: #selector(didTapCard(sender:))) labelsStack.addGestureRecognizer(tap).... @objc func didTapCard (sender: UITapGestureRecognizer) { (sender.view as? UIStackView)?.arrangedSubviews.forEach({ label in print((label as. UILabel).text) }) }

but the problem is that the click listener works only on the part of the stack view and when I tried to determine on which view we clicked it was not possible.

I think that possibly the problem is with that I tried to assign one click listener to several views, but not sure that works as I thought. I'm trying to make each label in the stackview clickable, but after click I will only need getting text from the label, so that is why I used one click listener for all views.

Applying a transform to a view (button, label, view, etc) changes the visual appearance , not the structure.

Because you're working with rotated views, you need to implement hit-testing.

Quick example:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    
    // convert the point to the labels stack view coordinate space
    let pt = labelsStack.convert(point, from: self)
    
    // loop through arranged subviews
    for i in 0..<labelsStack.arrangedSubviews.count {
        let v = labelsStack.arrangedSubviews[i]
        // if converted point is inside subview
        if v.frame.contains(pt) {
            return v
        }
    }

    return super.hitTest(point, with: event)
    
}

Assuming you're still working with the MyCustomView class and layout from your previous questions, we'll build on that with a few changes for layout, and to allow tapping the labels.

Complete example:

class Step5VC: UIViewController {
    
    // create the custom "left-side" view
    let myView = MyCustomView()
    
    // create the "main" stack view
    let mainStackView = UIStackView()

    // create the "bottom labels" stack view
    let bottomLabelsStack = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        guard let img = UIImage(named: "pro1") else {
            fatalError("Need an image!")
        }
        
        // create the image view
        let imgView = UIImageView()
        imgView.contentMode = .scaleToFill
        imgView.image = img
        
        mainStackView.axis = .horizontal
        
        bottomLabelsStack.axis = .horizontal
        bottomLabelsStack.distribution = .fillEqually
        
        // add views to the main stack view
        mainStackView.addArrangedSubview(myView)
        mainStackView.addArrangedSubview(imgView)
        
        // add main stack view and bottom labels stack view to view
        mainStackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(mainStackView)
        bottomLabelsStack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(bottomLabelsStack)

        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // constrain Top/Leading/Trailing
            mainStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            mainStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            //mainStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),

            // we want the image view to be 270 x 270
            imgView.widthAnchor.constraint(equalToConstant: 270.0),
            imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
            
            // constrain the bottom lables to the bottom of the main stack view
            //  same width as the image view
            //  aligned trailing
            bottomLabelsStack.topAnchor.constraint(equalTo: mainStackView.bottomAnchor),
            bottomLabelsStack.trailingAnchor.constraint(equalTo: mainStackView.trailingAnchor),
            bottomLabelsStack.widthAnchor.constraint(equalTo: imgView.widthAnchor),
            
        ])
        
        // setup the left-side custom view
        myView.titleText = "Gefährdung"
        
        let titles: [String] = [
            "keine / gering", "mittlere", "erhöhte", "hohe",
        ]
        let colors: [UIColor] = [
            UIColor(red: 0.863, green: 0.894, blue: 0.527, alpha: 1.0),
            UIColor(red: 0.942, green: 0.956, blue: 0.767, alpha: 1.0),
            UIColor(red: 0.728, green: 0.828, blue: 0.838, alpha: 1.0),
            UIColor(red: 0.499, green: 0.706, blue: 0.739, alpha: 1.0),
        ]
        
        for (c, t) in zip(colors, titles) {

            // because we'll be using hitTest in our Custom View
            //  we don't need to set .isUserInteractionEnabled = true
            
            // create a "color label"
            let cl = colorLabel(withColor: c, title: t, titleColor: .black)
            
            // we're limiting the height to 270, so
            // let's use a smaller font for the left-side labels
            cl.font = .systemFont(ofSize: 12.0, weight: .light)
            
            // create a tap recognizer
            let t = UITapGestureRecognizer(target: self, action: #selector(didTapRotatedLeftLabel(_:)))
            // add the recognizer to the label
            cl.addGestureRecognizer(t)

            // add the label to the custom myView
            myView.addLabel(cl)
        }
        
        // rotate the left-side custom view 90-degrees counter-clockwise
        myView.rotateTo(-.pi * 0.5)
        
        // setup the bottom labels
        let colorDictionary = [
            "Red":UIColor.systemRed,
            "Green":UIColor.systemGreen,
            "Blue":UIColor.systemBlue,
        ]
        
        for (myKey,myValue) in colorDictionary {
            // bottom labels are not rotated, so we can add tap gesture recognizer directly

            // create a "color label"
            let cl = colorLabel(withColor: myValue, title: myKey, titleColor: .white)

            // let's use a smaller, bold font for the left-side labels
            cl.font = .systemFont(ofSize: 12.0, weight: .bold)

            // by default, .isUserInteractionEnabled is False for UILabel
            //  so we must set .isUserInteractionEnabled = true
            cl.isUserInteractionEnabled = true
            
            // create a tap recognizer
            let t = UITapGestureRecognizer(target: self, action: #selector(didTapBottomLabel(_:)))
            // add the recognizer to the label
            cl.addGestureRecognizer(t)

            bottomLabelsStack.addArrangedSubview(cl)
        }
        
    }
    
    @objc func didTapRotatedLeftLabel (_ sender: UITapGestureRecognizer) {

        if let v = sender.view as? UILabel {
            let title = v.text ?? "label with no text"
            print("Tapped Label in Rotated Custom View:", title)
            // do something based on the tapped label/view
        }

    }
    
    @objc func didTapBottomLabel (_ sender: UITapGestureRecognizer) {

        if let v = sender.view as? UILabel {
            let title = v.text ?? "label with no text"
            print("Tapped Bottom Label:", title)
            // do something based on the tapped label/view
        }
        
    }
    
    func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
        let newLabel = PaddedLabel()
        newLabel.padding = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
        newLabel.backgroundColor = color
        newLabel.text = title
        newLabel.textAlignment = .center
        newLabel.textColor = titleColor
        newLabel.setContentHuggingPriority(.required, for: .vertical)
        return newLabel
    }
}



class MyCustomView: UIView {
    
    public var titleText: String = "" {
        didSet { titleLabel.text = titleText }
    }
    
    public func addLabel(_ v: UIView) {
        labelsStack.addArrangedSubview(v)
    }
    
    public func rotateTo(_ d: Double) {
        
        // get the container view (in this case, it's the outer stack view)
        if let v = subviews.first {
            // set the rotation transform
            if d == 0 {
                self.transform = .identity
            } else {
                self.transform = CGAffineTransform(rotationAngle: d)
            }
            
            // remove the container view
            v.removeFromSuperview()
            
            // tell it to layout itself
            v.setNeedsLayout()
            v.layoutIfNeeded()
            
            // get the frame of the container view
            //  apply the same transform as self
            let r = v.frame.applying(self.transform)
            
            wC.isActive = false
            hC.isActive = false
            
            // add it back
            addSubview(v)
            
            // set self's width and height anchors
            //  to the width and height of the container
            wC = self.widthAnchor.constraint(equalToConstant: r.width)
            hC = self.heightAnchor.constraint(equalToConstant: r.height)

            guard let sv = v.superview else {
                fatalError("no superview")
            }
            
            // apply the new constraints
            NSLayoutConstraint.activate([

                v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
                wC,
                
                outerStack.widthAnchor.constraint(equalTo: sv.heightAnchor),

            ])
        }
    }
    
    // our subviews
    private let outerStack = UIStackView()
    private let titleLabel = UILabel()
    private let labelsStack = UIStackView()
    
    private var wC: NSLayoutConstraint!
    private var hC: NSLayoutConstraint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        
        // stack views and label properties
        
        outerStack.axis = .vertical
        outerStack.distribution = .fillEqually
        
        labelsStack.axis = .horizontal
        // let's use .fillProportionally to help fit the labels
        labelsStack.distribution = .fillProportionally
        
        titleLabel.textAlignment = .center
        titleLabel.backgroundColor = .lightGray
        titleLabel.textColor = .white
        
        // add title label and labels stack to outer stack
        outerStack.addArrangedSubview(titleLabel)
        outerStack.addArrangedSubview(labelsStack)
        
        outerStack.translatesAutoresizingMaskIntoConstraints = false
        addSubview(outerStack)
        
        wC = self.widthAnchor.constraint(equalTo: outerStack.widthAnchor)
        hC = self.heightAnchor.constraint(equalTo: outerStack.heightAnchor)

        NSLayoutConstraint.activate([
            
            outerStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            outerStack.centerYAnchor.constraint(equalTo: self.centerYAnchor),
            wC, hC,
            
        ])
        
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        
        // convert the point to the labels stack view coordinate space
        let pt = labelsStack.convert(point, from: self)
        
        // loop through arranged subviews
        for i in 0..<labelsStack.arrangedSubviews.count {
            let v = labelsStack.arrangedSubviews[i]
            // if converted point is inside subview
            if v.frame.contains(pt) {
                return v
            }
        }

        return super.hitTest(point, with: event)
        
    }

}

class PaddedLabel: UILabel {
    var padding: UIEdgeInsets = .zero
    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.inset(by: padding))
    }
    override var intrinsicContentSize : CGSize {
        let sz = super.intrinsicContentSize
        return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
    }
}

I have written the same code as yours and it is working fine. Check the console output in the attached screenshot.

在此处输入图像描述

Code is as follows:

 override func viewDidLoad() {
    super.viewDidLoad()
   
    let stackView = UIStackView()
    stackView.axis = .vertical
    stackView.distribution = .fill
    stackView.alignment = .top
    stackView.spacing = 4

    let label1 = UILabel()
    label1.numberOfLines = 0
    label1.text = "Row 1"
    label1.tag = 11
    setTapListener(label1)
    stackView.addArrangedSubview(label1)
    
    let label2 = UILabel()
    label2.numberOfLines = 0
    label2.text = "Row 2"
    label2.tag = 22
    setTapListener(label2)
    stackView.addArrangedSubview(label2)

    stackView.translatesAutoresizingMaskIntoConstraints = false
    self.view.addSubview(stackView)
    stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    stackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
    stackView.heightAnchor.constraint(equalToConstant: 300).isActive = true
}

public func setTapListener(_ label: UILabel){
    let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
    tapGesture.numberOfTapsRequired = 1
    tapGesture.numberOfTouchesRequired = 1
    label.isUserInteractionEnabled = true
    label.addGestureRecognizer(tapGesture)
}

@objc func tapGestureMethod(_ gesture: UITapGestureRecognizer) {
    print(gesture.view?.tag ?? 0)
}

So there is no problem in the code, the problem may be in the UI. The UI may not be refreshed or some other element may be overlapping over it.

1- Try to use the view debugger to check whether any view or element is overlapping over it or not.

2- Try to refresh the layout using layoutSubviews to check whether issue persists or not.

Try to debug the issue with these techniques and solve if you find any. It should work fine.

  1. Read documentation: UITapGestureRecognizer is a class. It means that after calling function all internal variables will be destroyed. So just move let tapGesture: into the class above, do not create it in this function. For example:
   class ScrollView: UIScrollView {

   let tapGesture = {
      let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapGestureMethod(_:)))
      tapGesture.numberOfTapsRequired = 1
      tapGesture.numberOfTouchesRequired = 1
      label.isUserInteractionEnabled = true
      label.addGestureRecognizer(tapGesture)
      return tapGesture
    }()
    
    } 
  1. Why are you decided to use Label instead of UIButton (with transparence background color and border line)?
  2. Also you can use UITableView instead of stack & labels
  3. Maybe this documentation will help too (it is written that usually in one view better to keep one gesture recognizer): https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM