To diagnose these issues, use the view debugger. See the “Inspect and Resolve View Layout Issues” section of the Diagnosing and Resolving Bugs in Your Running App.
E.g. I fired this up the view debugger and see the problem in the view hierarchy:

And when I went to the runtime errors, it tells us what is wrong, namely that the “Scrollable content size is ambiguous for UIScrollView”:

And thus, because the error was in the scroll view’s contentSize
, I selected the scroll view’s subview, the stack view, and I can see that its frame
is zero (as shown in the first screen snapshot above). Because the button’s parent view has a zero size, the button will not receive the desired user interaction.
The root of the problem is that you should realize that there are two sets of constraints for scroll views, those to the frameLayoutGuide
(i.e. for the frame
of the scroll view, dictating how big the views are) and those to the contentLayoutGuide
(i.e. for the contentSize
of the scroll view, i.e. how much scrolling will happen).
If you add the missing constraints, the problem is resolved, e.g.:
NSLayoutConstraint.activate([
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: containerView.topAnchor),
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
])
Unrelated to that problem, I would suggest
The button’s clipToBounds
should be true
.
The cornerRadius
either be set to some predetermined value (e.g. 50 in this example) or that you have a rounded button subclass that does this corner radius change in layoutSubviews
. But you cannot set constraints and expect that to update the frame immediately.
The contentView
is a UIStackView
. I would therefore suggest:
- Use a name that reflects this, e.g.
stackView
.
- Do not just
addSubview
, but rather addArrangedSubview
to take advantage of the stack view’s features.
- If you do this, you can retire
view1
and just add the button directly, and no constraints are needed for the arranged subviews of a stack view.
Pulling this all together, you might have something like:
class ViewController: UIViewController {
let button : UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 100).isActive = true
button.heightAnchor.constraint(equalToConstant: 100).isActive = true
button.clipsToBounds = true
button.layer.cornerRadius = 50 // btn.frame.width / 2
button.layer.masksToBounds = true // false
button.setBackgroundImage(UIImage(named: "level01"), for: .normal)
button.tag = 1
button.addTarget(self, action: #selector(buttonClicked(_:)), for: .touchUpInside)
button.isUserInteractionEnabled = true
return button
}()
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.alignment = .center
stackView.axis = .vertical
return stackView
}()
let containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
setupViews()
}
@objc func buttonClicked(_ sender: UIButton) {
print("user tapped button")
}
func setupScrollView() {
view.addSubview(containerView)
containerView.addSubview(scrollView)
scrollView.addSubview(stackView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.topAnchor.constraint(equalTo: view.topAnchor),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: containerView.topAnchor),
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
])
}
func setupViews() {
stackView.addArrangedSubview(button)
}
}
(You didn’t show how the containerView
was defined, so I did it programmatically like everything else you are doing here. Clearly, if you defined it in IB, then those constraints would not been needed. Then, again, if I were defining it in IB, I'd do all of this in IB with zero code.)