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