Reputation: 283
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")
}
Upvotes: 23
Views: 23654
Reputation: 12294
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)
translatesAutoresizingMaskIntoConstraints = false
everywhere and always at all times every time
as always with a stack view, add and subtract views with .removeArrangedSubview
and .insertArrangedSubview
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.
Your top to bottom constraints (inside your shiny new holder view) MUST explicitly define the height of the holder view
Your hori constraints (inside your shiny new holder view) MUST explicitly GIVE A WIDTH TO the holder view
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.)
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?
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.
Upvotes: 0
Reputation: 20611
Make sure you add your view with addArrangedSubview
.
I had this issue because I added the button to the UIStackView
with addSubview
.
Upvotes: 2
Reputation: 1045
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! :]
Upvotes: 0
Reputation: 176
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).
Upvotes: 2
Reputation: 21
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.
Upvotes: 0
Reputation: 365
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://stackoverflow.com/a/42179529/344386 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.
Upvotes: 1
Reputation: 1022
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.
Upvotes: 1
Reputation: 79153
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.
Upvotes: 15
Reputation: 6109
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.
Upvotes: 3
Reputation: 23558
following theGuy's answer, I used the sherlock app and I immediately found the problem:
Upvotes: 4
Reputation: 2492
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.
Upvotes: 28
Reputation: 432
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!
Upvotes: 0
Reputation: 1252
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.
Upvotes: 45
Reputation: 679
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)
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.
Upvotes: 11