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?
To change a button's text color in SwiftUI we need to use . foregroundColor on our text view. In the above code it is almost the same as the simple button, but we used a modifier to change the foregroundColor to green. This needs to be on the Text and not on the Button view.
SwiftUI lets us disable any part of its forms or even the whole form, all by using the disabled() modifier. This takes a single Boolean that defines whether the element should be disabled or not. The form element's style automatically gets updated to reflect its status – buttons and toggles get grayed out, for example.
SwiftUI's button is similar to UIButton , except it's more flexible in terms of what content it shows and it uses a closure for its action rather than the old target/action system. To create a button with a string title you would start with code like this: Button("Button title") { print("Button tapped!") }
I guess you want this:
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 padding
s 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:
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 }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With