Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

SwiftUI Button: Custom Styles, Variants, and Best Practices

SwiftUI allows you to create buttons in different styles, both custom and default configurations. You can define reusable button styles or quickly get started using SwiftUI modifiers specifically available for buttons.

It’s essential to avoid tap gestures if possible since you’ll get a lot of accessibility support for free when using the button view element. Let’s dive into the details and explore the possibilities with buttons in SwiftUI.

How to use a button in SwiftUI?

You can use a button in SwiftUI using several default initializers and customizations. This article will cover techniques to define the following buttons:

A SwiftUI Button can appear in different styles using available APIs, customizations, and best practices.
A SwiftUI Button can appear in different styles using available APIs, customizations, and best practices.

The simplest version takes a title and an action to execute on tap:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Button("Press me!", action: {
            print("Button tapped!")
        })
    }
}

This results in a plain text button with a “Press me!” title. The title could be a plain string or a LocalizedStringKey to make it automatically translated based on your app’s translations. Like always in Swift, you can choose to use a trailing closure without the argument label:

Button("Press me!") {
    print("Button tapped!")
}

SwiftUI button with image

We can further enhance the button using an image. Ideally, you would use an SF Symbol for a high-quality recognizable icon:

Button("Press me!", systemImage: "play", action: {
    print("Button tapped!")
})

However, you can also decide to use a custom image loaded from a specific bundle:

Button("Press me!", image: ImageResource(name: "custom.crown", bundle: .main), action: {
    print("Button tapped!")    
})

You should use Bundle.main to load an image from your main app’s asset catalog or a custom bundle if you decide to load an image from, for example, a package.

Defining button roles

While primarily useful inside action sheets or alerts, you can also decide to use button roles in plain views. For example, you can use the destructive button role to indicate that a button results in a destructive action (for example, a deletion):

Button("Press me!", role: .destructive) {
    print("Button tapped!")    
}

When used inside alerts, these roles translate into proper styling to indicate to users what the button’s result will be. Another common role is the cancel role.

Get the code examples for this article

Join 19,381 developers that stay up to date using my weekly newsletter and get access to code examples for all my articles:

Using system button styles to match standards

The plain text SwiftUI buttons can be evolved into system-matching buttons with shapes. The above examples have used the automatic button style underneath:

Button("Press me!") {
    print("Button tapped!")
}.buttonStyle(.automatic)

The automatic button style in our examples translates into the .borderless button style:

Button("Press me!") {
    print("Button tapped!")
}.buttonStyle(.borderless)

There are several standard button styles to pick from. For example, the App Store download button uses the .borderedProminent button style in combination with capsule button bordered shape:

Button("Get") {
    print("Button tapped!")
}.buttonStyle(.borderedProminent)
    .buttonBorderShape(.capsule)

You can pick between different border shapes:

  • automatic
  • capsule
  • circle
  • roundedRectangle
  • roundedRectangle(radius:)

Each shape results in a slightly different button, and they’re all affected by the configured button style.

How to define a custom button style in SwiftUI?

For many apps, you want to define so-called custom system components that are reusable throughout your app. You’re aiming for design consistency, and button styles are a great way to achieve this. For example, you might want to apply a custom scale animation when a button is tapped to add an extra indication of a button that is pressed.

You can implement such scale-down animation using a custom button style in SwiftUI:

struct PrimaryButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
            .bold()
            .foregroundStyle(.white)
            .background(Color.accentColor)
            .clipShape(Capsule(style: .continuous))
            .scaleEffect(configuration.isPressed ? 0.9 : 1)
            .animation(.smooth, value: configuration.isPressed)
    }
}

Using Static Member Lookup in Generic Contexts, we can make this button style discoverable for autocompletion using the following definition:

extension ButtonStyle where Self == PrimaryButtonStyle {
    static var myAppPrimaryButton: PrimaryButtonStyle { .init() }
}

Finally, we can adjust our button to make use of our primary button style:

Button("Press me!") {
    print("Button tapped!")
}.buttonStyle(.myAppPrimaryButton)

A custom button style configuration object provides access to the following properties:

  • label: a type-erased label of the button as a SwiftUI view
  • isPressed: indicates whether the user is currently pressing the button
  • role: an optional semantic role that describes the button’s purpose.

The latter could be used to define a button style that changes based on the role:

struct RolesButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(configuration.role == .cancel ? .body.bold() : .body)
            .foregroundColor(configuration.role == .destructive ? Color.red : nil)
    }
}

Defining a custom label view for unique cases

In some cases, you want more control over the button’s styling only once. You can use a custom label view if you don’t have to reuse a button’s custom styling. For example, you might want to use an HStack to show an image and a title with a divider in between:

Button(action: {
    print("Button tapped!")
}, label: {
    HStack {
        Image("custom.crown")
        Divider()
        Text("Press me!")
    }
})

Defining both a custom button style and interaction

SwiftUI provides a PrimitiveButtonStyle for cases where you want to define both custom interactions and styling. You’ll have to manage all interactions yourself, meaning you must also manage when the button’s action will be triggered.

You’ll gain more control, but also more responsibility. Convenience properties like isPressed won’t be available anymore. An example use-case could be a button that changes state while being pressed and responds to both a single and double tap:

struct DoubleTapTriggerButtonStyle: PrimitiveButtonStyle {
    let doubleTapTrigger: () -> Void
    @GestureState private var isPressed = false
    
    /// A drag gesture that is solely used to keep tracked of the pressed state of the button.
    private var pressedStateGesture: some Gesture {
        DragGesture(minimumDistance: 0)
            .updating($isPressed) { _, isPressed, _ in
                guard !isPressed else { return }
                isPressed = true
            }
    }
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
            .bold()
            .foregroundStyle(.white)
            .background(Color.accentColor)
            .clipShape(Capsule(style: .continuous))
            .opacity(isPressed ? 0.75 : 1)
            .animation(.easeInOut, value: isPressed)
            
            /// Adding the gesture simultaneously to ensure we don't break the tap gestures.
            .simultaneousGesture(pressedStateGesture)
        
            /// Adding both tap gestures.
            .onTapGesture(count: 1, perform: configuration.trigger)
            .onTapGesture(count: 2, perform: doubleTapTrigger)
    }
}

/// Using Static Member Lookup in combination with a method to configure the double tap.
extension PrimitiveButtonStyle where Self == DoubleTapTriggerButtonStyle {
    static func doubleTapTrigger(action: @escaping () -> Void) -> DoubleTapTriggerButtonStyle {
        DoubleTapTriggerButtonStyle(doubleTapTrigger: action)
    }
}

Conclusion

A SwiftUI Button can be customized using standard view modifiers or custom button styles. You’ll be able to configure reusable designs, and you even have the option to add custom interactions to a button.

If you want to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

Are you ready to

Turn your side projects into independence?

Learn my proven steps to transform your passion into profit.