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.
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.
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 to disable / modify a button using SwiftUI?
Here is possible approach (see also comments in code). Tested & works with Xcode 11.2 / iOS 13.2.
struct ContentView: View {
@State private var buttonColor : Color = .blue
var body: some View {
VStack {
Button(action: {
self.buttonColor = .red
DispatchQueue.global(qos: .background).async { // do in background
test()
DispatchQueue.main.async {
self.buttonColor = .blue // work with UI only in main
}
}
}) {
Text("Start")
.font(.title)
.padding(.horizontal, 40)
.padding(.vertical, 5)
.background(self.buttonColor)
.foregroundColor(.white)
}
.disabled(self.buttonColor == .red) // disabled while calculating
}
}
}
SwiftUI: Disable/Re-Enable button based on WebView state
You're ending up doubling up the publisher property by defining them as @Published and CurrentValueSubject
.
The easiest fix would be to just make them Published
, which handles most of the work for you:
class ViewModel: ObservableObject {
@Published var canGoBack = false
@Published var canGoForward = false
}
//...
//In delegate:
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
viewModel.canGoBack = webView.canGoBack
viewModel.canGoForward = webView.canGoForward
}
//...
//In Navigation view:
Button(action: goBack) {
Image(systemName: "chevron.left").resizable().aspectRatio(contentMode: .fit)
}.disabled(!viewModel.canGoBack) //<-- here
.frame(width: 24, height: 24, alignment: .center).padding(.leading, 32)
Button(action: goForward) {
Image(systemName: "chevron.right").resizable().aspectRatio(contentMode: .fit)
}.disabled(!viewModel.canGoForward) //<-- here
.frame(width: 24, height: 24, alignment: .center)
You could still define them as CurrentValueSubject if you want (and ditch the @Published property wrapper), but there's probably no need to in this case.
Good SO question on the difference between @Published and CurrentValueSubject: Difference between CurrentValueSubject and @Published
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
Flipping The Positions of Items in a UIstackview
Protocol Associated Type Typealias Assignment Compile Error
How to Cast from Cftyperef to Axuielement in Swift
Uidatepicker Show Only Sunday's Date Only
Answers by Crashlytics - Adding Custom Event
Perform Segue After UIalertcontroller Is Dismissed
Storagemetadata' Has No Member 'Downloadurl'
Are Boolean Reads and Writes Guaranteed Atomic in Swift
Accessing Bundle of Main Application While Running Xctests
Variable with Getter/Setter Cannot Have Initial Value, on Overridden Stored Property
Sknode Subclass Generates Error: Cannot Invoke Initializer for Type "X" with No Arguments
Implement Protocol with Different Associated Type
Sizing a UIpickerview Inside a UIalertview