Daniil Subbotin
Daniil Subbotin

Reputation: 6708

How to set custom highlighted state of SwiftUI Button

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())

Upvotes: 63

Views: 60659

Answers (7)

Alexander Volkov
Alexander Volkov

Reputation: 8387

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)
            }
        }
    }
}

Upvotes: 2

Senseful
Senseful

Reputation: 91711

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()
  }
}

Upvotes: 1

BlueSpud
BlueSpud

Reputation: 1623

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 })
        }    
    }
}

Upvotes: 16

Ozan Honamlioglu
Ozan Honamlioglu

Reputation: 795

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)
        
    }))

Upvotes: 1

Anon Anon
Anon Anon

Reputation: 121

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")")
            }))
   }
} 

Upvotes: 2

Nico van der Linden
Nico van der Linden

Reputation: 79

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.

Upvotes: 7

arsenius
arsenius

Reputation: 13256

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())

Upvotes: 154

Related Questions