skip to Main Content

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.

3

Answers


    1. Sorry. My assumption was incorrect.
    2. Why are you decided to use Label instead of UIButton (with transparence background color and border line)?
    3. Also you can use UITableView instead of stack & labels
    4. 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
    Login or Signup to reply.
  1. The problem is with the the stackView’s height. Once the label is rotated, the stackview’s height is same as before and the tap gestures will only work within stackview’s bounds.

    I have checked it by changing the height of the stackview at the transform and observed tap gestures are working fine with the rotated label but with the part of it inside the stackview.

    Now the problem is that you have to keep the bounds of the label inside the stackview either by changing it axis(again a new problem as need to handle the layout with it) or you have to handle it without the stackview.

    You can check the observation by clicking the part of rotated label inside stackview and outside stackview.

    Code to check it:

    class ViewController: UIViewController {
    
    
    var centerLabel = UILabel()
    let mainStackView = UIStackView()
    var stackViewHeightCons:NSLayoutConstraint?
    var stackViewTopsCons:NSLayoutConstraint?
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        mainStackView.axis = .horizontal
        mainStackView.alignment = .top
        mainStackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(mainStackView)
        mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        stackViewTopsCons = mainStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 300)
        stackViewTopsCons?.isActive = true
        stackViewHeightCons = mainStackView.heightAnchor.constraint(equalToConstant: 30)
        stackViewHeightCons?.isActive = true
        
        centerLabel.textAlignment = .center
        centerLabel.text = "Let's rotate this label"
        centerLabel.backgroundColor = .green
        centerLabel.tag = 11
        
        setTapListener(centerLabel)
        mainStackView.addArrangedSubview(centerLabel)
        
        // outline the stack view so we can see its frame
        mainStackView.layer.borderColor = UIColor.red.cgColor
        mainStackView.layer.borderWidth = 1
        
    }
        
    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)
        var yCor:CGFloat = 300
        if centerLabel.transform == .identity {
            centerLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
            yCor = mainStackView.frame.origin.y - (centerLabel.frame.size.height/2)
        } else {
            centerLabel.transform = .identity
        }
        updateStackViewHeight(topCons: yCor)
    }
    
    private func updateStackViewHeight(topCons:CGFloat) {
        stackViewTopsCons?.constant = topCons
        stackViewHeightCons?.constant = centerLabel.frame.size.height
    }
    }
    
    Login or Signup to reply.
  2. 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)
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search