0

I am trying to a target to my button which is created by code like the following:

let btn : UIButton = {

    let btn = UIButton()
    btn.translatesAutoresizingMaskIntoConstraints = false
    btn.widthAnchor.constraint(equalToConstant: 100).isActive = true
    btn.heightAnchor.constraint(equalToConstant: 100).isActive = true
    btn.clipsToBounds = true
    btn.layer.cornerRadius = btn.frame.width / 2
    btn.layer.masksToBounds = false
    btn.setBackgroundImage(UIImage(named: "level01"), for: .normal)
    btn.tag = 1
    btn.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
    btn.isUserInteractionEnabled = true

    return btn
}()

This button is inserted in a view also created programmatically and inserted to a stack view then to scroll view.

let scrollView = UIScrollView()
let contentView = UIStackView()

override func viewDidLoad() {
    super.viewDidLoad()
    setupScrollView()
    setupViews()
}


@objc func buttonClicked () {
    print("user tapped button")
}

func setupScrollView(){
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    contentView.translatesAutoresizingMaskIntoConstraints = false

    self.view.addSubview(containerView)
    self.containerView.addSubview(scrollView)
    scrollView.addSubview(contentView)

    scrollView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true
    scrollView.widthAnchor.constraint(equalTo: containerView.widthAnchor).isActive = true
    scrollView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
    scrollView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true

    contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor).isActive = true
    contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
    contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
    contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
}

func setupViews(){
    contentView.addSubview(view1)
    view1.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
    view1.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
    view1.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 3/4).isActive = true
    view1.heightAnchor.constraint(equalToConstant: 100).isActive = true

    view1.addSubview(btn)
    view1.bringSubviewToFront(btn)
}

let view1: UIView = {
    let view = UIView()
    view.backgroundColor = UIColor.clear
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

When trying to hit the button it does nothing and I am asking If I am still missing something.

Thanks in advance for help.

Mohammed Aboelwafa
  • 1,035
  • 1
  • 9
  • 13
  • As far as I can see, there is no obvious problem why it shouldn't work. You should try to strip down the code to a minimal example, without all those scroll views in between, layouting etc. This will help understand the problem without getting lost in too much boilerplate code. – Andreas Oetjen Mar 07 '21 at 20:53

1 Answers1

0

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:

enter image description here

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

enter image description here

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.)

Rob
  • 415,655
  • 72
  • 787
  • 1,044