Search code examples
iosswiftuitapgesturerecognizer

How to handle UITapGestureRecognizer on views created in loops?


I have a vertical UIStackView and i append a horizontal UIStackView and some UILabels inside this view. I attach the UITapGestureRecognizer to handle the tap actions. How can i change for example the label(which is a subview) when i tap on a row?

Here is my code so far:

for article in articleType {
    //Create a row
    let row = UIStackView()
    row.axis = NSLayoutConstraint.Axis.horizontal
    
    //Create a label and indicator
    let label = UILabel()
    label.text = article.label

    let indicator = UILabel()
    indicator.text = "off"

    //Add label to row and row to parent stack view
    row.addSubView(label)
    row.addSubView(indicator)
    mainUiStackView.addArrangedSubview(row)

    //Create tap handled
    let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(sender:)))
    row.addGestureRecognizer(tap)
}

@objc func handleTap(sender: UITapGestureRecognizer? = nil) {
    print(sender.view)

    //This is where i'm stuck, how can i change the second label's text to "on" for example?
    sender.view.indicator.text = "on"

}

Solution

  • Your code goes through a loop and creates a whole series of stack views in the local variable row. Then you don't add those stack views to your view hierarchy, so they get "dropped on the floor."

    For this line:

    let row = UIStackView()
    

    After each pass through your for loop, that stack view you create will go out of scope and be deallocated.

    Edit:

    After you pointed it out, I see now where you add your row stack views to your mainUiStackView.

    I gather based on the comments you've made that you want to have taps on one of your "row" stack views set the indicator label in that stack view to "on".

    I would suggest building an array of your row stack views and their label views (As structs). Then find the row that was tapped and set it's label to the on state:

    struct RowStackViewStruct { 
       let stackView: UIStackView
       let label: UILabel
    }
    
    var rowStackViews = [RowStackViewStruct]()
    
    
    for article in articleType {
        //Create a row
        let row = UIStackView()
        row.axis = NSLayoutConstraint.Axis.horizontal
        
        //Create a label and indicator
        let label = UILabel()
        label.text = article.label
    
        let indicator = UILabel()
        indicator.text = "off"
    
        //Add label to row and row to parent stack view
        row.addSubView(label)
        row.addSubView(indicator)
        mainUiStackView.addArrangedSubview(row)
    
        //Create tap handled
        let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(sender:)))
        row.addGestureRecognizer(tap)
    
        //Build a RowStackViewStruct for this row
        let newRowStackViewStruct = RowStackViewStruct(stackView: row,
          label: indicator)
        rowStackViews.append(newRowStackViewStruct)
    }
    
    And then in your tap handler code:
    
    @objc func handleTap(sender: UITapGestureRecognizer? = nil) {
        print(sender.view)
    
        if let aStruct = rowStackViews.first(where: { sender?.view == $0.stackView }) {
           aStruct.label.text = "on"
    }
    

    I banged the above code out in the SO editor. It likely contains minor syntax errors, and is intended as a guide, not copy-paste code that you can drop in.

    It seems like you should really keep track of the on-off state of your rows. It seems like you should have an array of model objects that represent the state of your rows, and use that to build and maintain your main vertical stack view. In fact, it might be better to switch to a table view or collection view, since those are made to be driven from a model.