Unresponsive UIButton in subview added to UIStackView
Asked Answered
H

14

23

In the detailViewController of a UISplitView I have subviews added to a UIStackView inside a UIScrollView.

Just using system buttons without subviews or images, results in responsive buttons, but subviews seem to interfere.

Enabling touch is specifically coded. I have attempted to keep each view inside the containing view so there will be no overlap to invalidate receiving touch events, but not sure if this is done properly. Each subview contains a label and a custom button with an image. The subview is then added to the stackview, and the stackview to the scrollview.

Thanks for any help.

override func viewDidLoad() {
    super.viewDidLoad()

    scrollView = UIScrollView()
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(scrollView)

    // Constrain the scroll view within the detailView
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scrollView]|", options: .AlignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scrollView]|", options: .AlignAllCenterX, metrics: nil, views: ["scrollView": scrollView]))


    stackView = UIStackView()

    stackView.frame = CGRectMake(0,0,view.frame.width, 1000)
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.axis = .Vertical
    scrollView.contentSize = CGSizeMake(400, 1000)
    scrollView.addSubview(stackView)

    // Constrain the stackView within the scrollView
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[stackView]|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: ["stackView": stackView]))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[stackView]", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: ["stackView": stackView]))


    let selectedGroup: Group = GroupArray[5]

    let descriptorsArray = selectedGroup.descriptorsArray

    for descriptor in descriptorsArray {
        // Create a subview for each descriptor

        let subView = UIView()
        subView.frame = CGRectMake(0 , 0, self.stackView.frame.width-10, 54)
        subView.backgroundColor = UIColor.yellowColor()

        subView.heightAnchor.constraintEqualToConstant(54.0).active = true
        // Create a label for Descriptor subview

        let label = UILabel(frame: CGRectMake(20, 0, 200, 50))
        label.text = descriptor.name
        label.font = UIFont.boldSystemFontOfSize(22.0)
        label.textAlignment = .Left
        label.textColor = UIColor.brownColor()
        label.backgroundColor = UIColor.greenColor()
        label.heightAnchor.constraintEqualToConstant(50.0).active = true
        subView.addSubview(label)

        // Create a button for Checkbox
        let btn = UIButton()
        btn.frame = CGRectMake(220, 0, 50, 50)
        btn.backgroundColor = UIColor.blueColor()
        btn.setImage(UIImage(named:"checked.png"), forState: UIControlState.Normal)

        btn.heightAnchor.constraintLessThanOrEqualToConstant(50.0)
        btn.widthAnchor.constraintLessThanOrEqualToConstant(50.0)

        btn.addTarget(self, action: "btnPressed:", forControlEvents: UIControlEvents.TouchUpInside)

        subView.addSubview(btn)
        btn.userInteractionEnabled = true
        subView.userInteractionEnabled = true
        stackView.userInteractionEnabled = true
        scrollView.userInteractionEnabled = true
        stackView.addArrangedSubview(subView)


    }
}

func btnPressed(sender: UIButton!) {

    print("btn Pressed")

}
Haler answered 26/8, 2016 at 13:19 Comment(0)
E
11

It seems to be that something is lying over your button and catching all the touch events.

1. Never turn on userInterectionEnabled if you don't need it

There is no reason why a normal view like subView should have set userInteractionEnabled set to true so put it to false.

2. Find the bug:

To find out witch view catches the event start your app on a device and navigate to the stackView. Now in Xcode, press the button "Debug View Hierarchy" (placed right over the console output)

enter image description here

After that you will see every view currently displayed on your device. What you do now is finding out wich views are above your button, and then in code turn their userInteractionEnabled value to false.

Eyot answered 26/8, 2016 at 16:14 Comment(5)
M Kremer thank you so much for this debugging suggestion. It is the correct textbook and functional approach so I marked it as correct, but I failed to find the overlap using this approach. Since I knew stacked buttons worked while stacked subviews didn't (for me), my solution was to stack buttons instead of subviews with buttons in them. I changed the background of each button depending on state. Instead of a label, I set the title. No views to overlap as all should be contained in the button.Haler
The default value for all views is YES.Domel
the first advice does not make any sense, every time you put the view on the storyboard the user interaction is always set.Biota
your "debug view hierarchy" shows that everything is ok, "user interaction" is enabled for button and all its parent views. The button still doesn't work. Figured out that it is simply because it is inside stackview with alignment "leading". Changed alignment to "fill" and now everything works (even without of changes in hierarchy).Fontenot
@VyachaslavGerchicov thanks for the note, actually what you said is the same problem that i haveCanter
D
45

I have had the exact same issue: a StackView which contains views. Each view contains a button and a label. The views are only containers for layout reasons.

In my case: I did not give the view a height constraint and it seems that it had a zero height (after changing the background color for debugging I did not see the view), but the button and label was visible (but unresponsive).

After I have added a height constraint to the layout view the (debug) background color was visible and the buttons responded as expected.

Dirtcheap answered 10/10, 2017 at 13:7 Comment(5)
This saved me, thanks. I added a missing label.bottomAnchor.constraint(...) and everything started magically working again.Angeliqueangelis
I had the similar issue. I didn't set width constraint to the wrapper view so it has zero width in runtime.Shandy
same issue with me, I was missing height constraint. ThanksNightjar
I had to manually add a height constraint to my 3 view in a vertical UIStackView in a UIScrollView, which worked, but this is not good because the contents of my custom UIViews might change. I am surprised the UI displayed but the UITextfields and UIButtons weren't interact-able.Insalivate
@Insalivate YOU CAN - THE ISSUE IS POSSIBLY YOU'RE FORGETTING THE WIDTH!!!Allegiance
L
28

In my case, it was the aligment property of the StackView that was causing the problem.

When I set that property to Fill instead of Center the problem dissappeared.

Launder answered 27/12, 2018 at 9:43 Comment(4)
Thank you for this! I had tried everything I could think of and I would never have thought to try that!Alcuin
Thank you, Its help me.Volturno
It's this kind of stuff that UIKit does that makes me want to through my Mac through a window. Thanks for this answer!Spokeshave
Thanks! All I wanted to do was center a horizontal stack of buttons and it broke the whole dialog. No interaction at all.Lorenzalorenzana
R
14

I just ran into this issue myself. In my case, I had a UIStackView as a subview of my UIButton. I needed to set isUserInteractionEnabled = false on the stack view in order to get my button to work. This is probably a wise thing to set on any UIButton subview.

Roslynrosmarin answered 21/4, 2019 at 8:51 Comment(2)
I had the exact opposite problem, I had to set isUserInteractionEnabled = true on my UIStackView in order to get the button to register taps. This is a UIStackView I created programmatically.Curriculum
Oh wow.. This should be higher. This was cousing my problem also and was fixed with = falseIzzo
E
11

It seems to be that something is lying over your button and catching all the touch events.

1. Never turn on userInterectionEnabled if you don't need it

There is no reason why a normal view like subView should have set userInteractionEnabled set to true so put it to false.

2. Find the bug:

To find out witch view catches the event start your app on a device and navigate to the stackView. Now in Xcode, press the button "Debug View Hierarchy" (placed right over the console output)

enter image description here

After that you will see every view currently displayed on your device. What you do now is finding out wich views are above your button, and then in code turn their userInteractionEnabled value to false.

Eyot answered 26/8, 2016 at 16:14 Comment(5)
M Kremer thank you so much for this debugging suggestion. It is the correct textbook and functional approach so I marked it as correct, but I failed to find the overlap using this approach. Since I knew stacked buttons worked while stacked subviews didn't (for me), my solution was to stack buttons instead of subviews with buttons in them. I changed the background of each button depending on state. Instead of a label, I set the title. No views to overlap as all should be contained in the button.Haler
The default value for all views is YES.Domel
the first advice does not make any sense, every time you put the view on the storyboard the user interaction is always set.Biota
your "debug view hierarchy" shows that everything is ok, "user interaction" is enabled for button and all its parent views. The button still doesn't work. Figured out that it is simply because it is inside stackview with alignment "leading". Changed alignment to "fill" and now everything works (even without of changes in hierarchy).Fontenot
@VyachaslavGerchicov thanks for the note, actually what you said is the same problem that i haveCanter
F
4

following theGuy's answer, I used the sherlock app and I immediately found the problem:

enter image description here

Fretted answered 16/1, 2019 at 8:48 Comment(1)
that makes more sense than set user interactive false.Biota
D
3

In my case, I had set a custom view subclass in loadView. That subclass had isUserInteractionEnabled = false which translated down to all the sub views including my stackView and all its views and buttons.

Dachia answered 25/1, 2019 at 1:17 Comment(0)
S
2

I noticed a bug (I guess) in XCode 12. When my IBOutlet func (on swift) is empty, and I put a breakpoint inside the body of the empty func, or on the func declaration itself, then touch the button - XCode doesn't stop on my breakpoint (tested on Simulator).

Scandium answered 26/12, 2020 at 17:56 Comment(0)
B
2

Make sure you add your view with addArrangedSubview.

I had this issue because I added the button to the UIStackView with addSubview.

Bookplate answered 23/5, 2022 at 14:32 Comment(0)
D
1

In my case, it was just Xcode's bug. I've just closed and reopened it. It just worked. My current Xcode version is 11.4.1.

Dupe answered 1/6, 2020 at 12:32 Comment(0)
F
1

For me the problem was that I was instantiating views for Stack View from storyboard, and my ViewController was not added to a views structure, but UIView did. This why button actions from UIViewController subclass didn't work. So the answer of Mostafa Aghajani from here https://mcmap.net/q/527921/-uibutton-targets-not-working-if-a-uiview-is-added-as-a-subview helped me. If I do something like this:

    let testViewController = storyboard.instantiateViewController(withIdentifier: "TestViewController") as! TestViewController
    
    view.addSubview(testViewController.view) 
    // OR FOR STACK VIEW
    stackView.addArrangedSubview(testViewController.view)

Normally views structure when I check "XCode / Debug / View debugging / Capture view hierarchy" looks like UIViewController / UIView. But not in this case. The key was to do like this:

    let testViewController = storyboard.instantiateViewController(withIdentifier: "TestViewController") as! TestViewController
    self.addChild(testViewController) // this is the key

    view.addSubview(testViewController.view) 
    // OR FOR STACK VIEW
    stackView.addArrangedSubview(testViewController.view)

Then UIViewController was added in a views hierarchy and my UIButton inside of testViewController started to work. That's all is a kind of shamanism, may be I'm just doing something wrong.

Flotage answered 7/7, 2020 at 7:59 Comment(0)
P
0

I have had come cross the same issue. The reason is that i set button.alpha = 0, while setting the button.layer.opacity = 1.0. In this case, the button will not response to actions although it's visible. So setting button.alpha = 1.0 is the answer. May that help!

Picturize answered 12/6, 2018 at 7:24 Comment(0)
P
0

I had this happen in my stackview because the button went out of bounds of the stackview - but this was not obvious in the storyboard view. To confirm this as the issue, select the stackview and turn on "clips to bounds" in the attribute inspector to see if your button is going out of the stackview. You may have to check each stackview in the parent hierarchy all the way to the topmost stackview. Check this on all the phone sizes too - what might work on the iPhone 11 may clip on the iPhone4s.

Pointenoire answered 24/12, 2020 at 18:57 Comment(0)
I
0

Well, it is so obvious that you might not feel checking it necessary. I've spent maybe an hour trying all the other answers, and I want to save your time.

Check the code of your custom cell class, especially awakeFromNib method, to see if userInteractionEnabled of the cell is set to false somewhere. Set it to true. Voila! :]

Imam answered 26/1, 2022 at 4:13 Comment(0)
A
0

And don't forget the width also!!!!

The typical problem is you forgot to set constraints which explicitly set the height of the wrapper view.

However!

Very confusingly you also need to set constraints which explicitly set the WIDTH of the wrapper view

Hence, this example is incorrect

lazy var metaholder: UIView = {
    let holder = UIView()
    holder.translatesAutoresizingMaskIntoConstraints = false
    
    guard let sv = self.superview as? UIStackView else { return holder }
    let index = sv.arrangedSubviews.firstIndex(of: self)
    
    sv.removeArrangedSubview(self)
    
    holder.addSubview(self)
    holder.addSubview(heading)
    
    NSLayoutConstraint.activate([
        self.leftAnchor.constraint(equalTo: holder.leftAnchor),
        self.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
        
        heading.leftAnchor.constraint(equalTo: holder.leftAnchor),
        heading.topAnchor.constraint(equalTo: holder.topAnchor),
        
        self.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 5)
    ])
    
    sv.insertArrangedSubview(holder, at: index!)
    return holder
}()

The constraints should be, to see it working

    holder.widthAnchor.constraint(equalToConstant: 300),
    
    self.leftAnchor.constraint(equalTo: holder.leftAnchor),
    self.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
    
    heading.leftAnchor.constraint(equalTo: holder.leftAnchor),
    heading.topAnchor.constraint(equalTo: holder.topAnchor),
    
    self.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 5)]

and more correctly something like

    holder.widthAnchor.constraint(greaterThanOrEqualTo: self.widthAnchor),
    holder.widthAnchor.constraint(greaterThanOrEqualTo: heading.widthAnchor),
    
    self.leftAnchor.constraint(equalTo: holder.leftAnchor),
    self.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
    
    heading.leftAnchor.constraint(equalTo: holder.leftAnchor),
    heading.topAnchor.constraint(equalTo: holder.topAnchor),
    
    self.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 5)

List of issues:

  1. translatesAutoresizingMaskIntoConstraints = false everywhere and always at all times every time

  2. as always with a stack view, add and subtract views with .removeArrangedSubview and .insertArrangedSubview

  3. caveat on 2 as a general point in engineering stack views, be aware of the intricacies of .removeArrangedSubview - - notably, you typically also have to plain-old remove it from the view. A full understanding of these issues is beyond the scope of this QA.

  4. Your top to bottom constraints (inside your shiny new holder view) MUST explicitly define the height of the holder view

  5. Your hori constraints (inside your shiny new holder view) MUST explicitly GIVE A WIDTH TO the holder view

  6. It's easy to overlook point 5, since, many UIKit elements take care of themselves horizontally and you often don't need to bother. Your custom holder is not like this, you have to address the issue of width of the holder. (Apple should have thought of a "width propagating holder view", but of course obviously they didn't think of it. If you're a smartarse consider instead using a stack view in these cases, which is indeed a "width propagating holder view" after you spend two hours puzzling how to set it up correctly.)

  7. Incredibly confusingly in UIKit (call it a soft bug), the constraint solver can leave subviews of view X, all having correct (non zero) frames, BUT!!!!!!!!!!!, X itself can have a zero frame. In this case everything will display properly but you won't be able to click on the subviews. WTF right?

  8. Tap, tap, tap on Debug View Hierarchy in Xcode until your finger hurts. Make the holder view yellow or something and make sure you can see the holder view.

Allegiance answered 11/3 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.