Search code examples
swiftuibuttontargetgenerated

Programmatically generated UIButtons not responsive


I'm generating UIButtons dynamically and placing them in a dynamically sized UIImageView. I created the buttons in a loop, then in another loop I set their constraints based upon the available space . It all works, i.e., the buttons and their text show up nicely sorted BUT on clicking them nothing happens. button.isUserinteractionIsEnables = YES.

When logging the buttons in by final loop I they do not have Y,X coordinates. I think my anchors whould take care of that. Or is combining CGRect with anchors always a bad idea?

EDIT: Fixed On button's parentclass init I had to set .isUserInteractionEnabled = true; If at any point, a view has user interaction disabled, then it will refuse to perform its standard action, which includes not passing the event to any of its subviews

import UIKit

class SuggenstionCloud: UIImageView {

    override init(image: UIImage?) {
        super.init(image: image)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public func setConstraints(
        topAnchor : NSLayoutAnchor<NSLayoutYAxisAnchor>, topConstant: CGFloat,
        bottomAnchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, bottomConstant: CGFloat,
        trailingAnchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, trailingConstant: CGFloat,
        leadingAnchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, leadingConstant: CGFloat)
    {
        self.isUserInteractionEnabled = true
        self.contentMode = .scaleToFill
        self.topAnchor.constraint(equalTo: topAnchor, constant: topConstant).isActive = true;
        self.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomConstant).isActive = true;
        self.trailingAnchor.constraint(equalTo: trailingAnchor, constant: trailingConstant).isActive = true;
        self.leadingAnchor.constraint(equalTo:  leadingAnchor, constant: leadingConstant).isActive = true;
    }

    public func setLabels(weightedTags: [String: Int], selectedTags: [String]) {
        let buttons : [UIButton] = createButtons(weightedTags: weightedTags);
        createLayout(buttons: buttons)
    }

    private func createButton(buttonText: String) -> UIButton {
        let button = UIButton()
        button.setTitle(buttonText, for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.titleLabel?.font = UIFont(name: "Avenir-Light", size: 20.0)
        button.translatesAutoresizingMaskIntoConstraints = false;
        self.addSubview(button)
        button.frame = CGRect(x:0,y:0,width:button.intrinsicContentSize.width, height: button.intrinsicContentSize.height)
        button.addTarget(self, action: #selector(self.onButtonPresed(_:)), for: .touchUpInside);
        return button;
    }

    @objc func onButtonPresed(_ sender: UIButton) {
        // guard let label = sender.titleLabel else {return}
        print("Button : \(sender) poressed")
    }

    private func createButtons(weightedTags: [String: Int]) -> [UIButton] {
        var buttons : [UIButton] = [];
        for tag in weightedTags {
            buttons.append(createButton(buttonText: tag.key))
        }
       return buttons;
    }


    private func createLayout(buttons : [UIButton]) {
        if buttons.count == 0 { return }
        let topPadding : CGFloat = 30;
        let sidePadding : CGFloat = 32;

        let padding : CGFloat = 10;
        let availableHeight : CGFloat = self.frame.height + (-2 * topPadding)
        let availableWidth : CGFloat = self.frame.width + (-2 * sidePadding)

        var i = 0;

        var totalHeight : CGFloat = topPadding
        var rowLength : CGFloat = 0;
        var rowCount : Int = 0;
        var lastButton : UIButton!


        for button in buttons {
            if totalHeight > availableHeight { print("Cloud out of space"); return }
            let buttonWidth = button.intrinsicContentSize.width;
            let buttonHeight = button.intrinsicContentSize.height;

            if rowLength == 0 && rowCount == 0
            {
                print("FirstButtonLabel  \(String(button.titleLabel!.text!))")
                setFirstButtonConstraint(button: button, totalHeight: totalHeight, sidePadding: sidePadding)
                rowLength += buttonWidth + sidePadding + 5; // FIX annoying first row image overlap
            }
            else if rowLength + buttonWidth + padding < availableWidth
            {
                setConstraint(button: button, lastButton: lastButton, totalHeight: totalHeight, padding: padding)
                rowLength += buttonWidth + padding;
            }
            else
            {
                print("Out of space")
                totalHeight += buttonHeight + padding
                rowLength = buttonWidth + sidePadding;
                rowCount += 1;
                setNewRowConstraint(button: button, totalHeight:totalHeight , sidePadding: sidePadding)
            }
            i += 1;
            lastButton = button
            print(button.isUserInteractionEnabled)
            print(button.allTargets)
            print(button)
        }
    }
    private func setFirstButtonConstraint(button: UIButton, totalHeight: CGFloat, sidePadding: CGFloat) {
        button.topAnchor.constraint(equalTo: self.topAnchor, constant: totalHeight).isActive = true;
        button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: sidePadding + 5).isActive = true;
    }
    private func setConstraint(button: UIButton, lastButton: UIButton, totalHeight: CGFloat, padding:CGFloat) {
        button.leadingAnchor.constraint(equalTo: lastButton.trailingAnchor, constant: padding).isActive = true;
        button.topAnchor.constraint(equalTo: self.topAnchor, constant: totalHeight).isActive = true;
    }
    private func setNewRowConstraint(button: UIButton, totalHeight: CGFloat, sidePadding: CGFloat) {
        button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: sidePadding).isActive = true;
        button.topAnchor.constraint(equalTo: self.topAnchor, constant: totalHeight).isActive = true;
    }
}

The Print statements provide the following output:

true
[AnyHashable(<unFatMobile.SuggenstionCloud: 0x7fb5fae028e0; baseClass = UIImageView; frame = (10 280; 375 317); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x6000005de920>>)]
<UIButton: 0x7fb5fac2d840; frame = (0 0; 41 40); opaque = NO; layer = <CALayer: 0x6000005d0fe0>> 

I think I might be doing something wrong with the class lifecycle methods.


Solution

  • I see this has already been answered in the comments but a little background.

    When the user taps on the screen, it is the top level view that gets the event. If it has isUserInteractionEnabled set to true, then it performs its standard action (each UIView can do something different on a tap.) The base UIView's standard action is to figure out which subview was tapped on as pass the event to that subview. In this way, the tap event goes down the hierarchy of views from most general to most specific.

    If at any point, a view has user interaction disabled, then it will refuse to perform its standard action, which includes not passing the event to any of its subviews.

    Another way to accidentally make a UIButton not tapeable is to draw it outside its parent view's bounds. Views only pass events to sub-views that were drawn in their bounds.