Home > Mobile >  How to create a reusable button class with a stack view in UIKit?
How to create a reusable button class with a stack view in UIKit?

Time:05-22

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

CodePudding user response:

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.

CodePudding user response:

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

  • Related