Advanced Button Styles with SwiftUI

Designing a button that is visually pleasing with minimal code is certainly desirable when writing apps with SwiftUI. You may be familiar with the .buttonStyle() view modifier; perhaps you’ve only used this to give the button a PlainButtonStyle() like I had previously!

Creating a Custom ButtonStyle#

The below code is what I initially used to create my buttons in Plate-It. The makeBody() method is where all the magic happens, and you’ll find that the configuration argument that is passed in contains some very useful properties. configuration.label represents the view that the button has - in my case it’s just a Text view - and configuration.isPressed indicates whether the button is currently being pressed or not. We can use both of these to get the style we desire.

If you’ve dabbled in SwiftUI already, you’ll be familiar with most of the view modifiers on show already. However, we do use configuration.isPressed to change the background color of the button, reducing the opacity if it’s currently being tapped. This makes the button feel a little less ‘flat’.

public struct PrimaryButtonStyle: ButtonStyle {
    
    public func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .textCase(.uppercase)
            .foregroundColor(.white)
            .font(Font.body.weight(.semibold))
            .frame(maxWidth: .infinity, alignment: .center)
            .padding()
            .background(Color.accentColor.opacity(configuration.isPressed ? 0.7 : 1))
            .frame(height: 48)
            .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

The above can then be used like this across your app, and as you can see, it’s really easy to use!

struct ContentView: View {
    var body: some View {
        Button("Do The Thing") {
            doTheThing()
        }.buttonStyle(PrimaryButtonStyle())
    }
}

Take Your Buttons to the Next Level#

If your app makes a lot of network requests, you may want to disable your button or display a spinner to indicate that the request is ongoing. To do this, we can pass in a @Binding to a boolean that is a @State in our view.

Inside makeBody() we can use that @Binding to display a ProgressView() instead of the label, as well as disable the button to prevent duplicate taps from the user.

public struct PrimaryButtonStyle: ButtonStyle {
    
    @Binding private var showSpinner: Bool
    
    public init(showSpinner: Binding<Bool> = .constant(false)) {
        self._showSpinner = showSpinner
    }
    
    public func makeBody(configuration: Configuration) -> some View {
        Group {
            if showSpinner {
                ProgressView()
            } else {
                configuration.label
                    .textCase(.uppercase)
                    .foregroundColor(.white)
            }
        }
        .font(Font.body.weight(.semibold))
        .frame(maxWidth: .infinity, alignment: .center)
        .padding()
        .background(Color.accentColor.opacity(configuration.isPressed ? 0.7 : 1))
        .frame(height: 48)
        .clipShape(RoundedRectangle(cornerRadius: 16))
        .disabled(showSpinner)
    }
}

We can then expand our ContentView to set our state and pass it in to our button style:

struct ContentView: View {

    @State private var isSaving: Bool = false

    var body: some View {
        Button("Save The Thing") {
            saveTheThing()
        }.buttonStyle(PrimaryButtonStyle(showSpinner: $isSaving))
    }

    private func saveTheThing() {
        isSaving = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            isSaving = false
        }
    }
}

Conclusion#

If this tutorial helped you in any way, I’d really appreciate a share on Twitter. You can reach out to me via Twitter if you have any questions, or to show me what you’ve been up to!