13

SwiftUI Custom Button in List

I'm trying to create a custom button in a SwiftUI List. I want it to have a blue background with white text, and importantly, to remain blue and go to 50% opacity when pressed, not the default grey.

I tried using a custom ButtonStyle, but when I do so, the tappable area of the button is reduced to just the label itself. If I tap any other part of the cell, the colour doesn't change. If I remove the ButtonStyle, tapping anywhere on the cell works

How can I fix this so that I get my custom colours, including the colour when tapped, but the whole cell is still tappable?

import SwiftUI

struct BlueButtonStyle: ButtonStyle {

  func makeBody(configuration: Self.Configuration) -> some View {
    configuration.label
        .font(.headline)
        .foregroundColor(configuration.isPressed ? Color.white.opacity(0.5) : Color.white)
        .listRowBackground(configuration.isPressed ? Color.blue.opacity(0.5) : Color.blue)
  }
}

struct ExampleView: View {

    var body: some View {
        NavigationView {
            List {
                Section {
                    Text("Info")
                }

                Section {
                    Button(action: {print("pressed")})
                    {
                        HStack {
                            Spacer()
                            Text("Save")
                            Spacer()
                        }

                    }.buttonStyle(BlueButtonStyle())
                }
            }
            .listStyle(GroupedListStyle())
            .environment(\.horizontalSizeClass, .regular)
            .navigationBarTitle(Text("Title"))
        }
    }
}

struct ExampleView_Previews: PreviewProvider {
    static var previews: some View {
        ExampleView()
    }
}

2 Answers 2

19

In standard variant List intercepts and handles content area of tap detection, in your custom style it is defined, by default, by opaque area, which is only text in your case, so corrected style is

demo

Update for: Xcode 13.3 / iOS 15.4

It looks like Apple broken something, because listRowBackground now works only inside List itself, no subview, which is senseless from generic concept of SwiftUI.

Updated solution with same behavior as on demo

Original for: Xcode 11.4 / iOS 13.4

struct BlueButtonStyle: ButtonStyle {

  func makeBody(configuration: Self.Configuration) -> some View {
    configuration.label
        .font(.headline)
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
        .contentShape(Rectangle())
        .foregroundColor(configuration.isPressed ? Color.white.opacity(0.5) : Color.white)
        .listRowBackground(configuration.isPressed ? Color.blue.opacity(0.5) : Color.blue)
  }
}

and usage, just

Button(action: {print("pressed")})
{
    Text("Save")
}.buttonStyle(BlueButtonStyle())

and even

Button("Save") { print("pressed") }
    .buttonStyle(BlueButtonStyle())
Sign up to request clarification or add additional context in comments.

3 Comments

the listRowBackground() will no longer work, tested on iOS 15, any ideas how to solve? I am using it inside a Form
@Niklas try .background()
This is now a bit buggy in iOS 16/Xcode 14.2. The button highlight state doesn't animate all the time. @Asperi
0

For everybody experiencing the issue, that the press down animation doesn't work properly for custom buttons in a SwiftUI List, you can add a DragGesture to the Button, this will overwrite the Lists behavior of intercepting touches.

Button("Save") { print("pressed") }
    .buttonStyle(BlueButtonStyle())
    .gesture(DragGesture(minimumDistance: 0))

This gives you the immediate interaction back, however the default press down/up animations don't work anymore. So you need to handle that in the custom button style:

struct BlueButtonStyle: ButtonStyle {

  func makeBody(configuration: Self.Configuration) -> some View {
    configuration.label
        // ...
        .animation(configuration.isPressed ? nil : .easeOut(duration: 0.3), value: configuration.isPressed)
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.