SwiftUI ButtonStyle - how to check if button is disabled or enabled?
I found the answer thanks to this blog: https://swiftui-lab.com/custom-styling/
You can get the enabled state from the environment by creating a wrapper view and using it inside the style struct:
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: ButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
struct MyButton: View {
let configuration: ButtonStyle.Configuration
@Environment(\.isEnabled) private var isEnabled: Bool
var body: some View {
configuration.label.foregroundColor(isEnabled ? Color.green : Color.red)
}
}
}
This example demonstrates how to get the state and use it to change the appearance of the button. It changes the button text color to red if the button is disabled or green if it's enabled.
How can I know if a SwiftUI Button is enabled/disabled?
The whole idea of SwiftUI, is to avoid duplication of the source of truth. You need to think differently, and consider where the source of truth is. This is where you need to go to find out the button's state. Not from the button itself.
In "Data Flow Through SwiftUI", at minute 30:50, they explain that every piece of data has a single source of truth. If your button gets its state from some @Binding, @State, @EnvironmentObject, etc, your if statement should get that information from the same place too, not from the button.
Button won't show disabled styling
The @Environment is not injected in style, it is only for views, so here a demo of possible solution - based on internal helping wrapper view.
struct CustomButton: ButtonStyle {
enum ButtonStyle {
case button, destructive, light
}
enum ButtonSize {
case normal, large, small
}
private var maxWidth : CGFloat
private var padding : CGFloat
private var foreground : Color
private var foregroundDisabled : Color
private var strokeColor : Color
private var strokeDisabled : Color
private var strokeWidth : CGFloat
private var background : Color
private var backgroundDisabled : Color
private var fontSize : Font
init(style: ButtonStyle, size: ButtonSize) {
switch size {
case .large:
self.maxWidth = .infinity
self.padding = 15.0
self.fontSize = Font.system(.title3, design: .rounded).weight(.bold)
case .small:
self.maxWidth = 200
self.padding = 8.0
self.fontSize = Font.system(.callout, design: .rounded)
default:
self.maxWidth = .infinity
self.padding = 12.0
self.fontSize = Font.system(.body, design: .rounded)
}
switch style {
case .light:
strokeColor = .main
strokeDisabled = .gray
strokeWidth = size == .small ? strokeSmall: strokeLarge
foreground = .main
foregroundDisabled = .gray
background = .clear
backgroundDisabled = .clear
case .destructive:
strokeColor = .destructive
strokeDisabled = .gray
strokeWidth = size == .small ? strokeSmall : strokeLarge
foreground = .destructive
foregroundDisabled = .destructive
background = .clear
backgroundDisabled = .clear
default:
strokeColor = .clear
strokeDisabled = .clear
strokeWidth = 0.0
foreground = .white
foregroundDisabled = .white
backgroundDisabled = .gray
background = .main
}
}
private struct EnvReaderView<Content: View>: View { // << this one !!
let content: (Bool) -> Content
@Environment(\.isEnabled) var isEnabled // read environment
var body: some View {
content(isEnabled) // transfer into builder
}
}
func makeBody(configuration: Self.Configuration) -> some View {
EnvReaderView { isEnabled in // now we can use it !!
configuration.label
.frame(maxWidth: maxWidth)
.font(fontSize)
.foregroundColor(isEnabled ? foreground : Color.red)
.padding(padding)
.background(RoundedRectangle(cornerRadius: roundedCorner)
.strokeBorder(isEnabled ? strokeColor : strokeDisabled, lineWidth: strokeWidth)
.background(isEnabled ? background : backgroundDisabled)
)
.clipShape(RoundedRectangle(cornerRadius: roundedCorner))
.opacity(configuration.isPressed ? 0.8 : 1.0)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
}
}
}
How do I change button backgroundcolor if the button is disabled in swiftUI
To track .disabled
there is EnvironmentValues.isEnabled
that shows this state. But environment values are applicable only to views and do not work in style.
So the solution is to create custom button that tracks isEnabled and pass it into own style.
Below is a demo of solution approach (MyButtonStyle
is not changed). Tested with Xcode 12b.
struct MyButton: View {
let title: String
let action: () -> ()
@Environment(\.isEnabled) var isEnabled // to handle own state !!
init(_ title: String, action: @escaping () -> ()) {
self.title = title
self.action = action
}
var body: some View {
Button(title, action: action)
.buttonStyle(MyButtonStyle(enabledState: isEnabled))
}
}
struct ContentView: View {
@State private var buttonEnabled = true
var body: some View {
HStack {
MyButton("Button") { // << here !!
self.buttonEnabled.toggle()
print("Button pressed")
}
.disabled(!buttonEnabled) // << here !!
}
}
}
Changing the color of a button in SwiftUI based on disabled or not
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'saccentColor
when theButton
is enabled, and gray when theButton
is disabled. You don't have to compute the color at all.You can set the
accentColor
in the environment high up in yourView
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
}
Smartest way to set color for enabled/disabled buttons with SwiftUI
Actually, adding .buttonStyle(.plain)
to the button just did what I was looking for. Now, the button automatically gets a gray color if it's disabled.
Related Topics
Symbolicate Crash in iOS8 with Xcode 6 .1
Swift 3: How to Add Watermark on Video? Avvideocompositioncoreanimationtool iOS 10 Issue
iOS 10 Rich Media Push Notification (Media Attachment) in Objective-C
Is the Current Location/Compass Heading Button Available in the iOS Sdk
Not Receiving Any Push Notification in Iphone
A Server with the Specified Hostname Could Not Be Found
How to Wait for Method That Has Completion Block (All on Main Thread)
Continue Uploading Process in Background iOS
How to Play a Video from Either a Local or a Server Url in iOS
Core Data: Delete All Objects of an Entity Type, Ie Clear a Table
Multiple Checkmark When Row Selected in Uitableview iOS
Detect Uiimageview Touch in Swift
Find Size Contributed by Each External Library on iOS
How to Represent Core Data Optional Scalars (Bool/Int/Double/Float) in Swift
"Can't Find Model for Source Store" Occurring During iPhone "Automatic Lightweight Migration"