Architecting a consistent UI with reusable ButtonStyle’s in SwiftUI

Playstation controller buttons

Overview

Have you ever used an application that had no consistency in the UI? Where each button was shaped a little different or was colored weird and you simply didn’t know how to interact with the UI?

Well, you’re not alone.

Too many beginner app developers make the mistake of not creating a consistent UI for their users. Inconsistent buttons in your UI create awful user engagement and will lead to the ultimate DELETE button that your users know all too well (at least the apple designers didn’t mess that one up)!

Imagine if the user wanted to buy a million dollars worth of product but didn’t know how to check-out because each button was colored and shaped differently and didn’t fit a style. That would be awful! The scenario described, although filled with hyperbole, is more common than you might think.

Today I’m going to show you how to create consistency in your UI and remove redundant modifiers by using ButtonStyle's in SwiftUI.


What are ButtonStyle’s

In layman’s terms, ButtonStyles are a “view modifier” that we create to describe some type of custom look for a button in our application. The style that we create specifies the UI of the button (shape, foreground color, background color, etc) and the way the button updates the UI while it’s being clicked.

ButtonStyle‘s also gives us the ability to write once and use everywhere. As a result, we only have to write the modifiers for the ButtonStyle when we define it, and then every button within the app can conform to this custom style.

ButtonStyle‘s are a perfect example of DRY (don’t repeat yourself) in SWE. By repeating the view modifiers for each button within your app, every time you want to change the look of the button, you have to make multiple updates within your code. With a ButtonStyle, you can easily make changes in one place and see them reflected all throughout your app!

A good rule of thumb: If you see yourself using a custom-styled button multiple times within your app, it’s probably a good idea for you to look into creating a ButtonStyle.


Understanding ButtonStyle protocol

Now that we know what button Styles are, let’s take a look at how we can create them in our applications. Let’s take a look at the ButtonStyle protocol so we know what we have to implement in our custom button style.

/// A type that applies standard interaction behavior and a custom appearance to
/// all buttons within a view hierarchy.
///
/// To configure the current button style for a view hierarchy, use the
/// ``View/buttonStyle(_:)-7qx1`` modifier. Specify a style that conforms to
/// ``ButtonStyle`` when creating a button that uses the standard button
/// interaction behavior defined for each platform. To create a button with
/// custom interaction behavior, use ``PrimitiveButtonStyle`` instead.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ButtonStyle {

    /// A view that represents the body of a button.
    associatedtype Body : View

    /// Creates a view that represents the body of a button.
    ///
    /// The system calls this method for each ``Button`` instance in a view
    /// hierarchy where this style is the current button style.
    ///
    /// - Parameter configuration : The properties of the button.
    func makeBody(configuration: Self.Configuration) -> Self.Body

    /// The properties of a button.
    typealias Configuration = ButtonStyleConfiguration
}

As we can see from the protocol, our custom ButtonStyle struct needs to implement makeBody. MakeBody is called before the button is shown to the user to give the style to the body property of our button view. Consequently, MakeBody takes a configuration as a parameter and expects us to return a View containing the custom button label to be displayed to the user. Before we jump into the implementation, let’s see what this configuration provides us!

/// The properties of a button.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public struct ButtonStyleConfiguration {

    /// A type-erased label of a button.
    public struct Label : View {

        /// The type of view representing the body of this view.
        ///
        /// When you create a custom view, Swift infers this type from your
        /// implementation of the required `body` property.
        public typealias Body = Never
    }

    /// A view that describes the effect of pressing the button.
    public let label: ButtonStyleConfiguration.Label

    /// A Boolean that indicates whether the user is currently pressing the
    /// button.
    public let isPressed: Bool
}

We can see that there are two attributes: isPressed and label. Since label is of type View we can infer that this is the View displayed to our user. We can then use the isPressed parameter to create custom animations and UI changes when the button is pressed.

Great! We’re all ready to start writing the code for a custom ButtonStyle!


Creating your custom ButtonStyle

Now that we know what functions we need to implement for a custom ButtonStyle, let’s go ahead and create one!

For my custom button, I’m going to be creating a style for the buttons in my calculator app. All of my buttons are going to be circular (like the PlayStation controller above) and will get their UI attributes from an enum I created called CalculatorButton. By injecting a CalculatorButton enum case into the struct that implements ButtonStyle, we will be able to access all of the properties that define the UI within makeBody. Let’s see what that would look like:

struct CalculatorButtonStyle: ButtonStyle {
    let calculatorButton: CalculatorButton
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundColor(calculatorButton.backgroundColor)
            .clipShape(Circle())
            .overlay(
                Text(calculatorButton.symbol)
                    .font(.system(size: 36, weight: .medium, design: .default))
                    .foregroundColor(calculatorButton.symbolColor)
            )
    }
}

Although I chose not to add any crazy animations or changes when the button is pressed, we could easily add these changes by using the isPressed attribute. For example, if I wanted the button to be clear when it’s pressed, I could update the foregroundColor modifier to react to the button being pressed which can be seen below:

// example of using isPressed
.foregroundColor(configuration.isPressed ? .clear : calculatorButton.backgroundColor)

Here I simply display clear if the button is pressed and show the defaulted background color when it’s not. It’s as simple as that!

Now that we created our custom style, we can use this style on every calculator button on the control panel. Even better, if our designers tell us they want the font to be 32 and not 36, we only have to update it in ONE PLACE! Now that’s power! 💯


Using your custom ButtonStyle

Since we created a custom ButtonStyle, it’s probably best that I show you how to use them within your app.

Thankfully, it’s nearly pain-free. All you have to do is apply the buttonStyle view modifier to your button and pass in an instance of the ButtonStyle struct that we just created! Once you’re done it will look something like this ⬇️

Button(action: { print("I love UnwrappedBytes") }) {
    /// Your view here
}
.buttonStyle(CustomButtonStyle())

Woohoo! You just created your first ButtonStyle in SwiftUI 🥂


Wrap Up

Today you learned how to create a custom ButtonStyle in SwiftUI. ButtonStyle‘s make your code readable, maintainable, flexible, and they will even make your co-workers not despise reading your UI code.

I promise you will thank me in the future months when you start using custom styles within your application. ButtonStyles will save you so much time and frustration if you just take a few extra minutes to focus on solid SWE practices and use them within your SwiftUI codebase.

Let me know down below ⬇️ of any horror stories that you’ve had from codebases that didn’t use modular styling within their code. I know I have more than a few!

As always, if you liked today’s content, make sure you subscribe to the newsletter down below and if you want to support my coffee addiction, help me out by buying me a coffee! It keeps me going to create more AWESOME FREE CONTENT FOR YOU! As always, thanks for taking the time to unwrap some bytes with me. Cheers! 🍻

Processing…
Success! You're on the list.

Leave A Comment