In almost every bigger mobile project at some point there comes the moment when you decide not to repeat boilerplate UI code but extract it into nice and reusable components.

In this article, I will present an example of an approach which allows creating a very flexible and maintainable solution.

Once Upon a Time, there was a button.

Let’s start our journey with a story of a button - our designer decided about 2 visual styles that we need to implement in our product

Primary and secondary button example

Looks simple, let’s implement it.

class Custom1Button: UIButton {
    let blue = UIColor(red: 0/255.0, green: 122/255.0, blue: 255/255.0, alpha: 1)
    let darkerBlue = UIColor(red: 0/255.0, green: 98/255.0, blue: 250/255.0, alpha: 1)

    enum Style {
        case primary
        case secondary
    }

    private let style: Style

    init(
        frame: CGRect = .zero,
        style: Style
    ) {
        self.style = style
        super.init(frame: frame)
        setupStyle()
    }

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

    private func setupStyle() {
        switch style {
        case .primary:
            setupPrimaryStyle()
        case .secondary:
            setupSecondaryStyle()
        }
        updateStyle()
    }

    private func updateStyle() {
        switch style {
        case .primary:
            updatePrimaryStyle()
        case .secondary:
            updateSecondaryStyle()
        }
    }

    override var isHighlighted: Bool {
        didSet {
            updateStyle()
        }
    }

    private func setupPrimaryStyle() {
        contentEdgeInsets = .init(top: 14, left: 20, bottom: 14, right: 20)

        self.layer.cornerRadius = 8

        setTitleColor(.white, for: .normal)
        titleLabel?.font = UIFont.preferredFont(forTextStyle: .title3)
    }

    private func setupSecondaryStyle() {
        contentEdgeInsets = .init(top: 8, left: 12, bottom: 8, right: 12)

        self.backgroundColor = .white

        self.layer.borderColor = blue.cgColor
        self.layer.borderWidth = 0
        self.layer.cornerRadius = 8

        setTitleColor(blue, for: .normal)
        setTitleColor(darkerBlue, for: .highlighted)
        titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
    }

    private func updatePrimaryStyle() {
        self.backgroundColor = isHighlighted ? darkerBlue : blue
        updateShadow()
    }

    private func updateSecondaryStyle() {
        updateShadow()
    }

    private func updateShadow() {
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOffset = CGSize(width: 0, height: isHighlighted ? 0 : 1)
        layer.shadowOpacity = 0.5
        layer.shadowRadius = 2
    }
}

I decided to use an enum defining component style and base on its value - apply different UI properties.

Future World

Let’s think for a moment about the future of our project. We know that the creative team is full of ideas. So we may expect many new styles and variants for the button.

Also hopefully with product growth at some point, we will introduce modularization. So we can also expect keeping this code in a specialized framework. Maybe even maintained by other developers.

Buttons in the applications are ubiquitous. So choices that we will make today will stay with us for a long time.

Right now our implementation is a mix of presentation (colours, shadows) and logic responsible for setting and changing it. Every new style increases the amount of code in the class and if/else/switch block. Also, every change introduces a risk of regression and bugs.

So let’s discuss what we can do differently to make this component better.

The first step is separation of presentation from the logic. We can do it by creating a struct with all changable UI parameters. If something depends on button state - use a closure and pass the required state.

struct Style {
    let backgroundColor: (_ isHighlighted: Bool, _ button: UIButton) -> Void
    let shadow: (_ isHighlighted: Bool, _ button: UIButton) -> Void
    let contentEdgeInsets: UIEdgeInsets
    let title: (_ button: UIButton) -> Void
    let border: (_ button: UIButton) -> Void
}

You can see that we have 2 types of properties - ones used only at initialization and others reacting on highlight state. We can group them in two functions

extension Style {
    func setup(button: UIButton) {
        title(button)
        border(button)
        button.contentEdgeInsets = contentEdgeInsets
        isHighlighted(button.isHighlighted, button: button)
    }

    func isHighlighted(_ value: Bool, button: UIButton) {
        backgroundColor(value, button)
        shadow(value, button)
    }
}

Before we continue with implementation let’s quickly add a few helpers.

extension Style {
    static func shadow(isHighlighted: Bool, view: UIView) {
        view.layer.shadowColor = UIColor.black.cgColor
        view.layer.shadowOffset = CGSize(width: 0, height: isHighlighted ? 0 : 1)
        view.layer.shadowOpacity = 0.5
        view.layer.shadowRadius = 2
    }

    static func defaultBorder(view: UIView) {
        view.layer.cornerRadius = 8
    }

    static let blue = UIColor(red: 0/255.0, green: 122/255.0, blue: 255/255.0, alpha: 1)
    static let darkerBlue = UIColor(red: 0/255.0, green: 98/255.0, blue: 250/255.0, alpha: 1)
}

Ok, so we have our style. Now let’s define styles from the initial design - the primary style for the start.

extension Style {
	static let primary = Style(
        backgroundColor: { isHighlighted, button in
            button.backgroundColor = isHighlighted ? Self.darkerBlue : Self.blue
        },
        shadow: Self.shadow(isHighlighted:view:),
        contentEdgeInsets: .init(top: 14, left: 20, bottom: 14, right: 20),
        title: {
            $0.setTitleColor(.white, for: .normal)
            $0.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title3)
        },
        border: defaultBorder(view:)
    )
}

and then the secondary style

extension Style {
	static let secondary = Style(
        backgroundColor: { isHighlighted, button in
            button.backgroundColor = .white
        },
        shadow: Self.shadow(isHighlighted:view: ),
        contentEdgeInsets: .init(top: 8, left: 12, bottom: 8, right: 12),
        title: {
            $0.setTitleColor(blue, for: .normal)
            $0.setTitleColor(darkerBlue, for: .highlighted)
            $0.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
        },
        border: defaultBorder(view:)
    )
}

Now we can easly see what contains the “style” for each button, what are the differences between them and what is shared.

Ok, it’s time to implement our UIButton subclass.

After removing most of the “presentation” code from the first implementation, the class logic looks much simpler and cleaner.

 class Custom2Button: UIButton {
    private let style: Style

    init(
        frame: CGRect = .zero,
        style: Style
    ) {
        self.style = style
        super.init(frame: frame)
        style.setup(button: self)
    }

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

    override var isHighlighted: Bool {
        didSet {
            style.isHighlighted(isHighlighted, button: self)
        }
    }
}

This is promising.

For comparison let’s see how this solution compares with the initial approach. For purpose of this evaluation we will be implementing a new much different visual style - “a link”.

Expected link button result

With the monolith component, we need to add new methods

private func updateLinkStyle() { 
// ...
}

private func setupLinkStyle() {
// ...
}

Then we need to update Style enum and code in switch statment in updateStyle and setupStyle methods.

It’s 5 places when we need to change/add something to the UIButton subclass.

Using Style struct we need only one change - define the new static property in the extension

extension Style {
    static let link = Style(
        backgroundColor: { isHighlighted, button in
            button.backgroundColor = .clear
        },
        shadow: { _,_ in },
        contentEdgeInsets: .init(top: 8, left: 12, bottom: 8, right: 12),
        title: {
            $0.setTitleColor(blue, for: .normal)
            $0.setTitleColor(darkerBlue, for: .highlighted)
            $0.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
        },
        border: { _ in }
    )
}

Summary

As you can see with relatively simple refactoring we were able to make flexible and easy to extend component. With this approach, we can place our button in a framework and define styles in the main target or create a new private style only for a specific use case.

This is an example of Open-Closed Principle used in practice - “Software entities should be open for extension, but closed for modification.”

Sidenote

Why I’ve decided to use separate properties and not put everything into 2 methods / closures (setup & isHighlighted)?

Properties graduation helps to build more flexible and reusable struct. We can put the reusable code into a pure static function (for example func shadow) and reuse it. Another advantage - provide names for blocks of code which helps to read and understand code.

References

Source code