Button

One of the most ubiquitous views, using custom styling for a button can greatly affect the look and feel of your app.

  • Must use a style if you want an isPressed appearance
  • Inherit from ButtonStyle in almost all cases
  • Only inherit from PrimitiveButtonStyle to define your own interactions
  • Applies to NavigationLink unless it is in a List or Form
  • Link looks like a Button but you cannot change the style
  • Use the snippet below to see all of the built-in button styles
 1Button("Auto Button Style") { }
 2    .buttonStyle(.automatic)
 3
 4Button("Plain Button Style") { }
 5    .buttonStyle(.plain)
 6
 7Button("Bordered Button Style") { }
 8    .buttonStyle(.bordered)
 9
10Button("Bordered Button Style Prominent") { }
11    .buttonStyle(.borderedProminent)
12
13Button("Borderless Button Style") { }
14    .buttonStyle(.borderless)
  • You should prefer ButtonStyle over PrimitiveButtonStyle with few exceptions
  • No need to add gestures to work, everything is included

Create your style struct and have it conform to ButtonStyle. It is customary for all button styles to be named ...ButtonStyle. The Configuration type contains .label, .isPressed, and .role for you to use as you wish to style the button.

Here is a simple example of a custom button style.

 1struct MyButtonStyle: ButtonStyle {
 2    func makeBody(configuration: Configuration) -> some View {
 3        HStack(alignment: .firstTextBaseline) {
 4            image(for: configuration.role)
 5
 6            configuration.label
 7        }
 8        .overlay {
 9            if configuration.isPressed {
10                border
11            }
12        }
13    }
14
15    var border: some View {
16        RoundedRectangle(cornerRadius: 5)
17            .stroke(lineWidth: 3)
18    }
19
20    func image(for role: ButtonRole?) -> Image {
21        let systemName = switch role {
22            case .cancel: "xmark.circle"
23            case .destructive: "trash"
24            default: "face.smiling"
25        }
26
27        return Image(systemName: systemName)
28    }
29}

In order to use the nice .buttonStyle(.myButton) syntax, we must extend the ButtonStyle protocol. Without this, we would have to use .buttonStyle(MyButtonStyle()), which is now considered an old way of using styles.

1extension ButtonStyle where Self == MyButtonStyle {
2    static var myButton: Self { .init() }
3}

See the button style in action by using the following code in your view.

1Button("My Button Style") { }
2    .buttonStyle(.myButton)
3
4Button("My Button Style", role: .cancel) { }
5    .buttonStyle(.myButton)
6
7Button("My Button Style", role: .destructive) { }
8    .buttonStyle(.myButton)
  • Only inherit from PrimitiveButtonStyle when ButtonStyle is not sufficient
  • You must add a gesture to run the button's action

The Configuration type contains .label, .trigger, and .role for you to use as you wish to style the button.

Here is the setup.

 1struct MyPrimitiveButtonStyle: PrimitiveButtonStyle {
 2    func makeBody(configuration: Configuration) -> some View {
 3        configuration.label
 4            .foregroundStyle(color(for: configuration.role))
 5            .onTapGesture(perform: configuration.trigger)
 6    }
 7
 8    func color(for role: ButtonRole?) -> Color {
 9        switch role {
10            case .cancel: .orange
11            case .destructive: .purple
12            default: .green
13        }
14    }
15}
16
17extension PrimitiveButtonStyle where Self == MyPrimitiveButtonStyle {
18    static var myPrimitiveButton: Self { .init() }
19}

Here is an example usage.

1Button("My Primitive Button Style") { }
2    .buttonStyle(.myPrimitiveButton)
3
4Button("My Primitive Button Style", role: .cancel) { }
5    .buttonStyle(.myPrimitiveButton)
6
7Button("My Primitive Button Style", role: .destructive) { }
8    .buttonStyle(.myPrimitiveButton)

Note that this button does not have any kind of pressed appearance. If you want that, you will need to use a gesture that can run a closure when a gesture starts and when it ends in order to change a @State that affects the appearance. Have fun!

Keep in mind that the default button styling applies a transparency and saturation effect to a button while it is being pressed. If you exclude something like this then your custom button style may not "feel" right to your users. Also, there is an effect applied when a button when it is disabled and you would likely want to make it visually apparent that the button cannot be pressed. ButtonStyle will already disable hit testing on your button if the environment's isEnabled variable is false.

  • Under what contexts does the automatic style change?

Xcode Version: 16.1 beta 3
Official Apple Documentation