isak.me - a blog by isak solheim

Bi-weekly updates about things that interest me, things I have built and things I have learned.

iOS Custom Button with Label

GitHub contribution graph

Implementing a custom button featuring a label, using Swift5 and UIKit 🕹

Creating our button class

We start by creating a new file, ButtonWithLabel. To keep our button customizable, we initialize our class with a type and an action.

import UIKit

class ButtonWithLabel: UIView {
    typealias Action = (() -> Void)?
    private var action: (() -> Void)?

    enum ButtonWithLabelType {
        case info
        case alert
    }

    private var type: ButtonWithLabelType

    init(type: ButtonWithLabelType) {
        self.type = type
        super.init(frame: .null)
   }

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

    @discardableResult
    func with(action: Action) -> Self {
        self.action = action
        return self
    }

    @objc private func click() {
        action?()
    }
}

Creating the label and the icon

Because we want the entire class to be pressable, we create both our label and our button as UIButton elements, and we add the previously created click function as the buttons click-targets. I've also added some simple styling to the label button, giving it some padding and a background color. We will style the button in the next step.

private lazy var label: UIButton = {
    let button = UIButton()
    button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
    button.backgroundColor = .white.withAlphaComponent(0.8)
    button.addTarget(self, action: #selector(click), for: .touchUpInside)
    return button
}()

private lazy var icon: UIButton = {
    let button = UIButton()
    button.addTarget(self, action: #selector(click), for: .touchUpInside)
    return button
}()

Setting the label text and styling the icon

We want to create our label text and choose our icon based on the provided ButtonWithLabelType. To do this, we create a helper function, updateStyling(). In this example, I've used icons from SF Symbols.

private func updateStyling() {
    switch type {
    case .info:
        label.setTitle("Info", for: .normal)
        icon.setImage(UIImage(systemName: "questionmark.circle"), for: .normal)
    case .alert:
        label.setTitle("Alert", for: .normal)
        icon.setImage(UIImage(systemName: "exclamationmark.circle"), for: .normal)
    }
}

Positioning

We update our init function to call the newly created updateStyling. We also place our label and our icon in a UIStackView.

init(type: ButtonWithLabelType) {
    self.type = type
    super.init(frame: .null)
    updateStyling()

    let stack = UIStackView(arrangedSubviews: [label, icon])
    stack.alignment = .center

    addSubview(stack)
    stack.fillInSuperview()
}

Usage

We can now start using our new button by setting a type and giving it an action. In this example, I use it as a rightBarButtonItem.

let infoButton = ButtonWithLabel(type: .info).with(action: showInfoView)
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: infoButton)

And that's it! Here is the completed class:

import UIKit

class ButtonWithLabel: UIView {
    typealias Action = (() -> Void)?
    private var action: (() -> Void)?

    enum ButtonWithLabelType {
        case info
        case alert
    }

    private var type: ButtonWithLabelType

    private lazy var label: UIButton = {
        let button = UIButton()
        button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
        button.backgroundColor = .white.withAlphaComponent(0.8)
        button.addTarget(self, action: #selector(click), for: .touchUpInside)
        return button
    }()

    private lazy var icon: UIButton = {
        let button = UIButton()
        button.addTarget(self, action: #selector(click), for: .touchUpInside)
        return button
    }()

    init(type: ButtonWithLabelType) {
        self.type = type
        super.init(frame: .null)
        updateStyling()

        let stack = UIStackView(arrangedSubviews: [label, icon])
        stack.alignment = .center

        addSubview(stack)
        stack.fillInSuperview()
    }

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

    private func updateStyling() {
        switch type {
        case .info:
            label.setTitle("Info", for: .normal)
            icon.setImage(UIImage(systemName: "questionmark.circle"), for: .normal)
        case .alert:
            label.setTitle("Alert", for: .normal)
            icon.setImage(UIImage(systemName: "exclamationmark.circle"), for: .normal)
        }
    }

    @discardableResult
    func with(action: Action) -> Self {
        self.action = action
        return self
    }

    @objc private func click() {
        action?()
    }
}