swiftswiftui

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?


Solution

  • 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:

    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
    }