Reputation: 833
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
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
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
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
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
Reputation: 386018
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
}
Upvotes: 81
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