How to set custom highlighted state of SwiftUI Button
Asked Answered
V

7

62

I have a Button. I want to set custom background color for highlighted state. How can I do it in SwiftUI?

enter image description here

Button(action: signIn) {
    Text("Sign In")
}
.padding(.all)
.background(Color.red)
.cornerRadius(16)
.foregroundColor(.white)
.font(Font.body.bold())
Verticillaster answered 8/6, 2019 at 19:48 Comment(0)
D
147

Updated for SwiftUI beta 5

SwiftUI does actually expose an API for this: ButtonStyle.

struct MyButtonStyle: ButtonStyle {

  func makeBody(configuration: Self.Configuration) -> some View {
    configuration.label
      .padding()
      .foregroundColor(.white)
      .background(configuration.isPressed ? Color.red : Color.blue)
      .cornerRadius(8.0)
  }

}


// To use it
Button(action: {}) {
  Text("Hello World")
}
.buttonStyle(MyButtonStyle())

Dairy answered 11/7, 2019 at 0:25 Comment(5)
I'm getting this: dyld: Symbol not found: _$s7SwiftUI11ButtonStyleP4body13configuration9isPressed4BodyQzAA0C0VyAA0cD5LabelVG_SbtFTqOporto
This doesn't configure the label of the Button rather than the Button itself? The behaviour I'm seeing is suggestive of configuration.label affecting a subview, say if one was to set a corner radius in the style but a background on the Button following the .buttonStyle(...) callInelastic
Is there any to way to make custom disabled style? The one that is applied with .disabled(true)?Pratte
@PavelAlexeev See https://mcmap.net/q/131391/-swiftui-buttonstyle-how-to-check-if-button-is-disabled-or-enabled for a solution to your problem.Neoprene
This is only for pressed state not selected, which is just a momentary state: "A Boolean that indicates whether the user is currently pressing the button." developer.apple.com/documentation/swiftui/…Ofilia
L
15

As far as I can tell, theres no officially supported way to do this as of yet. Here is a little workaround that you can use. This produces the same behavior as in UIKit where tapping a button and dragging your finger off of it will keep the button highlighted.

struct HoverButton<Label: View>: View {
    
    private let action: () -> ()
    
    private let label: () -> Label
    
    init(action: @escaping () -> (), label: @escaping () -> Label) {
        self.action = action
        self.label = label
    }
    
    @State private var pressed: Bool = false
    
    var body: some View {
        Button(action: action) {
            label()
                .foregroundColor(pressed ? .red : .blue)
                .gesture(DragGesture(minimumDistance: 0.0)
                    .onChanged { _ in self.pressed = true }
                    .onEnded { _ in self.pressed = false })
        }    
    }
}
Layman answered 9/6, 2019 at 1:6 Comment(5)
Nice find! It can also be accomplished using .longPressAction(minimumDuration: 0, maximumDistance: .infinity, {}) { pressed in self.pressed = pressed } }.Accouplement
however, this doesn't animate, does it? :hmm:Uphemia
@Uphemia Use withAnimation{}.Stratagem
Does this work if the button is somewhere within a scroll view?Selfseeker
no it does not @DavidAnderson I switched back to the normal Button because of thisInsincere
C
7

I was looking for a similar functionality and I did it in the following way.

I created a special View struct returning a Button in the style I need, in this struct I added a State property selected. I have a variable named 'table' which is an Int since my buttons a round buttons with numbers on it

struct TableButton: View {
    @State private var selected = false

    var table: Int

    var body: some View {
        Button("\(table)") {
            self.selected.toggle()
        }
        .frame(width: 50, height: 50)
        .background(selected ? Color.blue : Color.red)
        .foregroundColor(.white)
        .clipShape(Circle())
    }
}

Then I use in my content View the code

HStack(spacing: 10) {
  ForEach((1...6), id: \.self) { table in
    TableButton(table: table)
  }
}

This creates an horizontal stack with 6 buttons which color blue when selected and red when deselected.

I am not a experienced developer but just tried all possible ways until I found that this is working for me, hopefully it is useful for others as well.

Carrew answered 4/2, 2020 at 19:51 Comment(0)
A
2

This is for the people who are not satisfied with the above solutions, as they raise other problems such as overlapping gestures(for example, it's quite hard to use this solution in scrollview now). Another crutch is to create a custom button style like this

struct CustomButtonStyle<Content>: ButtonStyle where Content: View {
    
    var change: (Bool) -> Content
    
    func makeBody(configuration: Self.Configuration) -> some View {
        return change(configuration.isPressed)
    }
}

So, we should just transfer the closure which will return the state of the button and create the button based on this parameter. It will be used like this:

struct CustomButton<Content>: View where Content: View {
    var content:  Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    var body: some View {
        Button(action: { }, label: {
            EmptyView()
        })
            .buttonStyle(CustomButtonStyle(change: { bool in
                Text("\(bool ? "yo" : "yo2")")
            }))
   }
} 
Achates answered 23/6, 2020 at 19:58 Comment(1)
Could you please show an example of using CustomButton?Jacobsen
L
2

You need to define a custom style that can be used to provide the two backgrounds for normal and highlighted states:

Button(action: {
     print("action")
}, label: {
     Text("My Button").padding()
})
.buttonStyle(HighlightableButtonStyle(normal: { Color.red },
                                      highlighted: { Color.green }))

// Custom button style
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct HighlightableButtonStyle<N, H>: ButtonStyle where N: View, H: View {
    
    private let alignment: Alignment
    private let normal: () -> N
    private let highlighted: () -> H
    
    init(alignment: Alignment = .center, @ViewBuilder normal: @escaping () -> N, @ViewBuilder highlighted: @escaping () -> H) {
        self.alignment = alignment
        self.normal = normal
        self.highlighted = highlighted
    }
    
    func makeBody(configuration: Configuration) -> some View {
        return ZStack {
            if configuration.isPressed {
                configuration.label.background(alignment: alignment, content: highlighted)
            }
            else {
                configuration.label.background(alignment: alignment, content: normal)
            }
        }
    }
}
Lilian answered 2/6, 2022 at 7:1 Comment(0)
J
1

Okey let me clear everything again. Here is the exact solution

  1. Create the below button modifier.
    struct StateableButton<Content>: ButtonStyle where Content: View {
        var change: (Bool) -> Content
        
        func makeBody(configuration: Configuration) -> some View {
            return change(configuration.isPressed)
        }
    }
  1. Then use it like below one
    Button(action: {
        print("Do something")
    }, label: {

        // Don't create your button view in here
        EmptyView()
    })
    .buttonStyle(StateableButton(change: { state in

        // Create your button view in here
        return HStack {
            Image(systemName: "clock.arrow.circlepath")
            Text(item)
            Spacer()
            Image(systemName: "arrow.up.backward")
        }
        .padding(.horizontal)
        .frame(height: 50)
        .background(state ? Color.black : Color.clear)
        
    }))
Jam answered 12/5, 2021 at 12:30 Comment(0)
U
1

The cleanest solution is going to use the ButtonStyle's configuration.isPressed property, but keep all the rendering logic in the default location for a button. Since it seems like ButtonStyle is a level above the label, I assumed you could use the Environment for this. In my testing though, I wasn't able to get it to work.

I was however able to get it to work via preference keys:

struct IsPressedButtonStyleExample: View {
  @State private var isPressed: Bool = false

  var body: some View {
    Button(action: {}, label: {
      Text("Button")
        .foregroundColor(isPressed ? Color.red : Color.blue )
    })
    .buttonStyle(IsPressedButtonStyle())
    .onPreferenceChange(IsPressedButtonStyleKey.self) { isPressed = $0 }
  }
}

struct IsPressedButtonStyle_Previews: PreviewProvider {
  static var previews: some View {
    IsPressedButtonStyleExample()
  }
}

struct IsPressedButtonStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .preference(key: IsPressedButtonStyleKey.self, value: configuration.isPressed)
  }
}

struct IsPressedButtonStyleKey: PreferenceKey {
  static var defaultValue: Bool = false

  static func reduce(value: inout Bool, nextValue: () -> Bool) {
    value = nextValue()
  }
}
Ume answered 22/7, 2023 at 22:55 Comment(1)
As convoluted as this answer is (if only you could pass a bindinging into the buttonstyle that would actually get set/used correctly [I tried]) it is the only solution that really works. The DragGesture hack causes scrolling issues. LongPressGesture seems to only work on press and not release. The others require you to put your UI inside the button modifier (not very portable/ideal.)Ingrained

© 2022 - 2024 — McMap. All rights reserved.