skip to Main Content

I’m using a class to build a reusable button (image below) that uses a stack view to position two labels vertically, and allows me to configure both labels’ text when called.

enter image description here

I tried to add "label" and "subLabel" into a UIStackView in the init method below, but the stack isn’t being added onto the button’s view.

What would be the best way to integrate a stack view into this custom button class?

struct ActivityButtonVM {
    let labelText: String
    let subLabelText: String
    let action: Selector
}

final class ActivityButton: UIButton {
    private let label: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.textColor = .black
        
        return label
    }()
    
    private let subLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.textColor = .gray
        
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)

        setBackgroundImage(Image.setButtonBg, for: .normal)
        
        let stack = UIStackView(arrangedSubviews: [label, subLabel])
        stack.axis = .vertical
        stack.alignment = .center
        addSubview(stack)
        clipsToBounds = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(with viewModel: ActivityButtonVM) {
        label.text = viewModel.labelText
        subLabel.text = viewModel.subLabelText
        self.addTarget(SetActivityVC(), action: viewModel.action,
                       for: .touchUpInside)
    }
}

This is how I’m using this custom button class:

class SetActivityVC: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }
    
    lazy var firstButton: UIButton = {
        let button = ActivityButton()
        button.configure(with: ActivityButtonVM(labelText: "No Exercise", subLabelText: "no exercise or very infrequent", action: #selector(didTapFirst))
        return button
    }()
    
    lazy var secondButton: UIButton = {
        let button = ActivityButton()
        button.configure(with: ActivityButtonVM(labelText: "Light Exercise", subLabelText: "some light cardio/weights a few times per week", action: #selector(didTapSecond))
        return button
    }()
    
    @objc func didTapFirst() {
        print("Tapped 1")
    }
    
    @objc func didTapSecond() {
        print("Tapped 2")
    }
}

extension SetActivityVC {
    fileprivate func setupViews() {
        addViews()
        constrainViews()
    }
    
    fileprivate func addViews() {
        view.addSubview(firstButton)
        view.addSubview(secondButton)
    }
    
    fileprivate func constrainViews() {
        firstButton.centerXToSuperview()
        
        secondButton.centerXToSuperview()
        secondButton.topToBottom(of: firstButton, offset: screenHeight * 0.03)
    }
}

2

Answers


  1. First, you are not calling your init(frame:) when initialising your buttons:

    let button = ActivityButton()
    

    You are just calling the initialiser you inherited from NSObject, so of course the stack views are not added.

    You can add a parameterless convenience initialiser yourself, that calls self.init(frame:):

    convenience init() {
        self.init(frame: .zero)
    }
    

    and then the stack views will be added.

    I think you would also need to add:

    stack.translatesAutoresizingMaskIntoConstraints = false
    

    to stop the autoresizing mask constraints from causing the stack view to have a .zero frame.

    Additionally, you should add constraints to the stack view so that it is positioned correctly with respect to the button. (probably pin the 4 sides to the button’s 4 sides?)

    Last but not least, the way that you are adding the target is incorrect. You are adding a new instance of SetActivityVC as the target here, rather than the instance of the VC that has the button.

    self.addTarget(SetActivityVC(), action: viewModel.action,
        for: .touchUpInside)
    

    Instead, if you want to do this with target-action pairs, you should include the target in the view model as well:

    struct ActivityButtonVM {
        let labelText: String
        let subLabelText: String
        let target: Any // <----
        let action: Selector
    }
    
    ...
    
    self.addTarget(viewModel.target, action: viewModel.action,
        for: .touchUpInside)
    

    Tip: rather than using colours such as .black and .gray, use .label and .secondaryLabel so that it also looks good in dark mode.

    Login or Signup to reply.
  2. You can use alternative way: new UIButton.configuration, declare your buttons:

    let myButton1 = UIButton()
    let myButton2 = UIButton()
    let myButton3 = UIButton()
    let myButton4 = UIButton()
    

    now add this extension for button configuration:

    extension UIViewController {
    
    func buttonConfiguration(button: UIButton, config: UIButton.Configuration, title: String, subtitle: String, bgColor: UIColor, foregColor: UIColor, imageSystemName: String, imageTintColor: UIColor) {
        let b = button
        b.configuration = config
        b.configuration?.title = title
        b.configuration?.titleAlignment = .center
        b.configuration?.subtitle = subtitle
        b.configuration?.baseForegroundColor = foregColor
        b.configuration?.baseBackgroundColor = bgColor
        b.configuration?.image = UIImage(systemName: imageSystemName)?.withTintColor(imageTintColor, renderingMode: .alwaysOriginal)
        b.configuration?.imagePlacement = .top
        b.configuration?.imagePadding = 6
        b.configuration?.cornerStyle = .large
    }
    

    how to use, in viewDidLoad set your buttons and relative targets:

    buttonConfiguration(button: myButton1, config: .filled(), title: "My Button One", subtitle: "This is first button", bgColor: colorUpGradient, foregColor: .white, imageSystemName: "sun.min", imageTintColor: .orange)
        myButton1.addTarget(self, action: #selector(didTapFirst), for: .touchUpInside)
        
    buttonConfiguration(button: myButton2, config: .filled(), title: "My Button Two", subtitle: "This is second button", bgColor: .fuxiaRed, foregColor: .white, imageSystemName: "cloud", imageTintColor: .white)
        myButton2.addTarget(self, action: #selector(didTapSecond), for: .touchUpInside)
        
    buttonConfiguration(button: myButton3, config: .filled(), title: "My Button Tree", subtitle: "This is third button", bgColor: .celesteCiopChiaro, foregColor: .black, imageSystemName: "cloud.drizzle", imageTintColor: .red)
    myButton3.addTarget(self, action: #selector(didTapThird), for: .touchUpInside)
        
    buttonConfiguration(button: myButton4, config: .filled(), title: "My Button Four", subtitle: "This is four button", bgColor: .darkYellow, foregColor: .black, imageSystemName: "cloud.bolt", imageTintColor: .black)
        myButton4.addTarget(self, action: #selector(didTapFour), for: .touchUpInside)
    

    set your stackView and constraints:

    let stackView = UIStackView(arrangedSubviews: [myButton1, myButton2, myButton3, myButton4])
        stackView.axis = .vertical
        stackView.spacing = 12
        stackView.distribution = .fillEqually
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(stackView)
        stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        stackView.heightAnchor.constraint(equalToConstant: 372).isActive = true // 84(height of single button) * 4(number of buttons) = 336 + 36(total stackView spaces from buttons) = 372(height of intere stackView)
        stackView.widthAnchor.constraint(equalToConstant: view.frame.width - 60).isActive = true // set width of button
    

    add buttons functions:

    @objc func didTapFirst() {
           print("Tapped 1")
       }
    
    @objc func didTapSecond() {
            print("Tapped 2")
        }
    @objc func didTapThird() {
           print("Tapped 3")
       }
    
    @objc func didTapFour() {
            print("Tapped 4")
        }
    

    This is the result:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search