SwiftUI Button Image When Pressed

How do you change the image a SwiftUI button shows while the user is pressing the button?

Button State

A UIKit control like a UIButton has a default normal state and additional states that indicate if it’s highlighted, disabled, selected or focused. You can provide different configurations for each state. You can configure the button to show a different image, title, or background when the user touches (highlights) it. That provides a useful visual feedback to the user.

A SwiftUI button lacks a lot of the UIKit customizations. I’ve written about SwiftUI button styles and shapes before. The ButtonStyleConfiguration gives you access to the button label, isPressed state, and button role (cancel or destructive).

I wanted to create the equivalent of a UIKit info button that uses the “info.circle” SF Symbol:

info button, i in a circle

When the user presses the button I want the image to switch the filled variant of the symbol:

info button, i in a filled circle

Pressed Button Style

Since this is something I’m going to want to reuse for other buttons I built a pressed button style that accepts a title, and two images for the default and pressed states:

struct PressedButtonStyle: ButtonStyle {
  let title: String
  let systemImage: String
  let pressedImage: String

  func makeBody(configuration: Configuration) -> some View {
    let imageName = configuration.isPressed ? pressedImage : systemImage
    return Label(title, systemImage: imageName)
  }
}

In the makeBody method I check the button configuration and return a label with the correct image based on the isPressed state. I don’t like how the style ignores the button’s configured label but I don’t see another way?

Then to build my InfoButton:

struct InfoButton: View {
  let action: () -> Void

  init(_ action: @escaping () -> Void) {
    self.action = action
  }

  var body: some View {
    Button("") {
      action()
    }
    .buttonStyle(
      PressedButtonStyle(
        title: "Info",
        systemImage: "info.circle",
        pressedImage: "info.circle.fill")
    )
    .padding()
  }
}

Using the button:

InfoButton {
  // Do something
}
.labelStyle(.iconOnly)
.font(.largeTitle)

Adding Animation

One extra refinement, adding an SF Symbol effect to the PressedButtonStyle to scale the image up when pressed:

struct PressedButtonStyle: ButtonStyle {
  let title: String
  let systemImage: String
  let pressedImage: String

  func makeBody(configuration: Configuration) -> some View {
    let imageName = configuration.isPressed ? pressedImage : systemImage
    return Label(title, systemImage: imageName)
      .symbolEffect(.scale.up, isActive: configuration.isPressed)
  }
}

Maybe Apple will improve the button configuration to make this easier but until then that’s the best I can manage. If you have a better approach let me know.

Read More