mg_berk
mg_berk

Reputation: 833

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

I have a textfield with a send button that's a systemImage arrow. I want the foreground color of the image to change depending on whether the textField is empty or not. (I.e. the button is gray, and it is disabled if the textfield is empty. It's blue if the count of the textfield text is > 1).

I have a workaround that's not perfect:


if chatMessageIsValid {
    Spacer()
    HStack {
        TextField($chatMessage, placeholder: Text("Reply"))
            .padding(.leading, 10)
            .textFieldStyle(.roundedBorder)
        Button(action: sendMessage) {
            Image(systemName: "arrow.up.circle")
                .foregroundColor(Color.blue)
                .padding(.trailing, 10)
        }.disabled(!chatMessageIsValid)
    }
} else {
    Spacer()
    HStack {
        TextField($chatMessage, placeholder: Text("Reply"))
            .padding(.leading, 10)
            .textFieldStyle(.roundedBorder)
        Button(action: sendMessage) {
            Image(systemName: "arrow.up.circle")
                .foregroundColor(Color.gray)
                .padding(.trailing, 10)
        }.disabled(!chatMessageIsValid)
    }
}

This almost works, and it does change the color of the image if the text is > 1 in length. However, due to the change in state you're kicked out of editing the textfield after one character is typed, and you'll need to select the textfield again to continue typing. Is there a better way to do this with the .disabled modifier?

Upvotes: 55

Views: 56790

Answers (6)

Slavcho
Slavcho

Reputation: 2812

You can add .buttonStyle(.plain) in order to grey out your button while disabled. But if you want to use custom colour, then take the approach from the other solutions.

Upvotes: 0

Jeremy
Jeremy

Reputation: 3149

With these various solutions, you can use the \.isEnabled environment property instead of creating custom button styles or passing in disabled booleans yourself.

@Environment(\.isEnabled) private var isEnabled

Upvotes: 47

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 120072

All modifier arguments can be conditional:

.foregroundColor(condition ? .red : .yellow)

where the condition could be any true/false

var condition: Bool { chatMessage.isEmpty } // can be used inline

You can use any other condition you need

Upvotes: 4

Jayden Irwin
Jayden Irwin

Reputation: 977

If you want to customize the button even further with disabled (or any other) state, you can add variables to your custom ButtonStyle struct.

Swift 5.1

struct CustomButtonStyle: ButtonStyle {
    var disabled = false
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
        .background(disabled ? .red : .blue)
    }
}

// SwiftUI
@State var isDisabled = true
var body: some View {
    Button().buttonStyle(CustomButtonStyle(disabled: self.isDisabled))
}

Upvotes: 16

rob mayoff
rob mayoff

Reputation: 386018

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
}

Upvotes: 81

FreeNickname
FreeNickname

Reputation: 7823

Try this

Spacer()
HStack {
    TextField($chatMessage, placeholder: Text("Reply"))
        .padding(.leading, 10)
        .textFieldStyle(.roundedBorder)
    Button(action: sendMessage) {
        Image(systemName: "arrow.up.circle")
            .foregroundColor(chatMessageIsValid ? Color.blue : Color.gray)
            .padding(.trailing, 10)
        }.disabled(!chatMessageIsValid)
}

I suppose, the reason TextField loses focus is that in your code the whole view hierarchy is changed depending on the value of chatMessageIsValid. SwiftUI doesn't understand that the view hierarchy in the then block is almost identical to the one in the else block, so it rebuilds it completely.

In this edited code it should see that the only thing that changes is the foregroundColor of the image and the disabled status of the button, leaving the TextField untouched. Apple had a similar examples in one of the WWDC videos, I believe.

Upvotes: 8

Related Questions