Swiftui Buttonstyle - How to Check If Button Is Disabled or Enabled

SwiftUI ButtonStyle - how to check if button is disabled or enabled?

I found the answer thanks to this blog: https://swiftui-lab.com/custom-styling/

You can get the enabled state from the environment by creating a wrapper view and using it inside the style struct:

struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: ButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}

struct MyButton: View {
let configuration: ButtonStyle.Configuration
@Environment(\.isEnabled) private var isEnabled: Bool
var body: some View {
configuration.label.foregroundColor(isEnabled ? Color.green : Color.red)
}
}
}

This example demonstrates how to get the state and use it to change the appearance of the button. It changes the button text color to red if the button is disabled or green if it's enabled.

How can I know if a SwiftUI Button is enabled/disabled?

The whole idea of SwiftUI, is to avoid duplication of the source of truth. You need to think differently, and consider where the source of truth is. This is where you need to go to find out the button's state. Not from the button itself.

In "Data Flow Through SwiftUI", at minute 30:50, they explain that every piece of data has a single source of truth. If your button gets its state from some @Binding, @State, @EnvironmentObject, etc, your if statement should get that information from the same place too, not from the button.

Button won't show disabled styling

The @Environment is not injected in style, it is only for views, so here a demo of possible solution - based on internal helping wrapper view.

struct CustomButton: ButtonStyle {

enum ButtonStyle {
case button, destructive, light
}
enum ButtonSize {
case normal, large, small
}

private var maxWidth : CGFloat
private var padding : CGFloat
private var foreground : Color
private var foregroundDisabled : Color
private var strokeColor : Color
private var strokeDisabled : Color
private var strokeWidth : CGFloat
private var background : Color
private var backgroundDisabled : Color
private var fontSize : Font

init(style: ButtonStyle, size: ButtonSize) {
switch size {
case .large:
self.maxWidth = .infinity
self.padding = 15.0
self.fontSize = Font.system(.title3, design: .rounded).weight(.bold)
case .small:
self.maxWidth = 200
self.padding = 8.0
self.fontSize = Font.system(.callout, design: .rounded)
default:
self.maxWidth = .infinity
self.padding = 12.0
self.fontSize = Font.system(.body, design: .rounded)
}

switch style {
case .light:
strokeColor = .main
strokeDisabled = .gray
strokeWidth = size == .small ? strokeSmall: strokeLarge
foreground = .main
foregroundDisabled = .gray
background = .clear
backgroundDisabled = .clear
case .destructive:
strokeColor = .destructive
strokeDisabled = .gray
strokeWidth = size == .small ? strokeSmall : strokeLarge
foreground = .destructive
foregroundDisabled = .destructive
background = .clear
backgroundDisabled = .clear
default:
strokeColor = .clear
strokeDisabled = .clear
strokeWidth = 0.0
foreground = .white
foregroundDisabled = .white
backgroundDisabled = .gray
background = .main
}
}

private struct EnvReaderView<Content: View>: View { // << this one !!
let content: (Bool) -> Content
@Environment(\.isEnabled) var isEnabled // read environment
var body: some View {
content(isEnabled) // transfer into builder
}
}

func makeBody(configuration: Self.Configuration) -> some View {
EnvReaderView { isEnabled in // now we can use it !!
configuration.label
.frame(maxWidth: maxWidth)
.font(fontSize)
.foregroundColor(isEnabled ? foreground : Color.red)
.padding(padding)
.background(RoundedRectangle(cornerRadius: roundedCorner)
.strokeBorder(isEnabled ? strokeColor : strokeDisabled, lineWidth: strokeWidth)
.background(isEnabled ? background : backgroundDisabled)
)
.clipShape(RoundedRectangle(cornerRadius: roundedCorner))
.opacity(configuration.isPressed ? 0.8 : 1.0)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
}
}
}

How do I change button backgroundcolor if the button is disabled in swiftUI

To track .disabled there is EnvironmentValues.isEnabled that shows this state. But environment values are applicable only to views and do not work in style.

So the solution is to create custom button that tracks isEnabled and pass it into own style.

Below is a demo of solution approach (MyButtonStyle is not changed). Tested with Xcode 12b.

struct MyButton: View {

let title: String
let action: () -> ()

@Environment(\.isEnabled) var isEnabled // to handle own state !!

init(_ title: String, action: @escaping () -> ()) {
self.title = title
self.action = action
}

var body: some View {
Button(title, action: action)
.buttonStyle(MyButtonStyle(enabledState: isEnabled))
}
}

struct ContentView: View {
@State private var buttonEnabled = true

var body: some View {
HStack {
MyButton("Button") { // << here !!
self.buttonEnabled.toggle()
print("Button pressed")
}
.disabled(!buttonEnabled) // << here !!
}
}
}

Changing the color of a button in SwiftUI based on disabled or not

I guess you want this:

demo

You can add a computed property for the button color, and pass the property to the button's foregroundColor modifier. You can also use a single padding modifier around the HStack instead of separate paddings on its subviews.

struct ContentView : View {
@State var chatMessage: String = ""

var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
.foregroundColor(buttonColor)
}
.disabled(!chatMessageIsValid)
}
.padding([.leading, .trailing], 10)
}

var chatMessageIsValid: Bool {
return !chatMessage.isEmpty
}

var buttonColor: Color {
return chatMessageIsValid ? .accentColor : .gray
}

func sendMessage() {
chatMessage = ""
}
}

However, you shouldn't use the foregroundColor modifier at all here. You should use the accentColor modifier. Using accentColor has two benefits:

  • The Image will automatically use the environment's accentColor when the Button is enabled, and gray when the Button is disabled. You don't have to compute the color at all.

  • You can set the accentColor in the environment high up in your View hierarchy, and it will trickle down to all descendants. This makes it easy to set a uniform accent color for your whole interface.

In the following example, I put the accentColor modifier on the HStack. In a real app, you would probably set it on the root view of your entire app:

struct ContentView : View {
@State var chatMessage: String = ""

var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle")
}
.disabled(!chatMessageIsValid)
}
.padding([.leading, .trailing], 10)
.accentColor(.orange)
}

var chatMessageIsValid: Bool {
return !chatMessage.isEmpty
}

func sendMessage() {
chatMessage = ""
}
}

Also, Matt's idea of extracting the send button into its own type is probably smart. It makes it easy to do nifty things like animating it when the user clicks it:

button animation demo

Here's the code:

struct ContentView : View {
@State var chatMessage: String = ""

var body: some View {
HStack {
TextField($chatMessage, placeholder: Text("Reply"))
.textFieldStyle(.roundedBorder)
SendButton(action: sendMessage, isDisabled: chatMessage.isEmpty)
}
.padding([.leading, .trailing], 10)
.accentColor(.orange)
}

func sendMessage() {
chatMessage = ""
}
}

struct SendButton: View {
let action: () -> ()
let isDisabled: Bool

var body: some View {
Button(action: {
withAnimation {
self.action()
self.clickCount += 1
}
}) {
Image(systemName: "arrow.up.circle")
.rotationEffect(.radians(2 * Double.pi * clickCount))
.animation(.basic(curve: .easeOut))
}
.disabled(isDisabled)
}

@State private var clickCount: Double = 0
}

Smartest way to set color for enabled/disabled buttons with SwiftUI

Actually, adding .buttonStyle(.plain) to the button just did what I was looking for. Now, the button automatically gets a gray color if it's disabled.



Related Topics



Leave a reply



Submit