20

There is no isEnabled property for a SwiftUI button. How can i tell if it is enabled?

In regular UIKit, i would simply do

if button.isEnabeld == true {
} else {
}

but there is no SwiftUI equivalent.

Just a coder
  • 15,480
  • 16
  • 85
  • 138
  • are you using any library ? share details code . – Nahid Raihan Jul 02 '19 at 09:18
  • I am using SwiftUI released by Apple Xcode 11 Beta (no external library). There is a Button. I want to know how to tell if it enabled/disabled. There is no code to add. The question is literally this simple. – Just a coder Jul 02 '19 at 09:20
  • *You control* if a button is enabled or not, with `.disabled(someBooleanState)`. See https://www.hackingwithswift.com/quick-start/swiftui/enabling-and-disabling-elements-in-forms for examples. – Martin R Jul 02 '19 at 09:37
  • 1
    @MartinR i can use this if i am going to set the button’s state my self. But in this case, the button’s state is already set at some other point in my code. The button is now passed to functionX. Now Inside functionX, I wanted to know what is the state of the button? – Just a coder Jul 02 '19 at 09:38
  • 1
    @iOSCalendarpatchthecode.com The button state is generated from your app state. Check the app state, don't check state of UI. – Sulthan Jul 02 '19 at 09:52
  • @Sulthan hmm... ok, then this makes functionX a lot more complicated i think. FunctionX was only meant to do some simple UI modifications etc, but now it has to check app state (which i assume is using @environment or something). Since we have a `Button.disable(Bool)` function, is there really no way to check the state of the button? – Just a coder Jul 02 '19 at 10:01
  • 1
    I don't know a better way to say this, but it's only more complicated if you don't think in terms of "push" instead of "pull". It sounds like you're fighting a base concept of `SwiftUI`. As @sulthan said, why should you query the UI about something that is owned by the app? –  Jul 02 '19 at 10:06

5 Answers5

34

Inside a view, if you wish to react to the state set by .disabled(true), you can use: @Environment(\.isEnabled) var isEnabled

Since the environment can be used from within a View or a ViewModifier, this can be used to change layout properties of a view based on the state set from outside.

Unfortunately, ButtonStyle cannot directly use @Environment, but you can use a ViewModifier to inject environment values into a ButtonStyle in order to use the value from within a ButtonStyle:


// First create a button style that gets the isEnabled value injected
struct MyButtonStyle: ButtonStyle {
    private let isEnabled: Bool
    init(isEnabled: Bool = true) {
        self.isEnabled = isEnabled
    }
    func makeBody(configuration: Configuration) -> some View {
        return configuration
            .label
            .background(isEnabled ? .green : .gray)
            .foregroundColor(isEnabled ? .black : .white)
    }
}

// Then make a ViewModifier to inject the state
struct MyButtonModifier: ViewModifier {
    @Environment(\.isEnabled) var isEnabled
    func body(content: Content) -> some View {
        return content.buttonStyle(MyButtonStyle(isEnabled: isEnabled))
    }
}

// Then create a convenience function to apply the modifier
extension Button {
    func styled() -> some View {
        ModifiedContent(content: self, modifier: MyButtonModifier())
    }
}

// Finally, try out the button and watch it respond to it's state
struct ContentView: View {
    var body: some View {
        Button("Test", {}).styled().disabled(true)
    }
}

You can use this method to inject other things into a ButtonStyle, like size category and theme.

I use it with a custom style enum that contains all the flavours of button styles found in our design system.

  • Nice answer, you can simplify the solution by creating `MyButton` which takes the `configuration` as an input. In the `MyButtonStyle.makeBody` return `MyButton(configuration: configuration)`. Inside the button you can access use the Environment to check if it is enabled. So the `MyButton` has all the logic, `MyButtonStyle` is just a wrapper – user1046037 Apr 27 '20 at 13:28
  • Hi @user1046037 Do you mind laying this out to code? – Dreiohc Sep 20 '21 at 06:47
  • Nevermind found the answer. Thanks! https://stackoverflow.com/a/59210692/15585355 – Dreiohc Sep 20 '21 at 07:45
24

From outside a view you should know if you used .disabled(true) modifier.

From inside a view you can use @Environment(\.isEnabled) to get that information:

struct MyButton: View {
    let action: () -> Void
    @Environment(\.isEnabled) private var isEnabled

    var body: some View {
        Button(action: action) {
            Text("Click")
        }
        .foregroundColor(isEnabled ? .green : .gray)
    }
}

struct MyButton_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            MyButton(action: {})
            MyButton(action: {}).disabled(true)
        }
    }
}
12

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.

kontiki
  • 37,663
  • 13
  • 111
  • 125
  • based on the comments above, i think you are correct. Thanks, i will rethink my logic – Just a coder Jul 02 '19 at 10:22
  • 1
    You're welcome. If you haven't already, the 37 minutes of the WWDC session Data Flow Through SwiftUI is time well spent. You will probably save a lot of time down the road. Highly recommended! Cheers. – kontiki Jul 02 '19 at 10:24
  • To be honest, this works in every type of UI, including Cocoa/CocoaTouch. It's not really a different type of thinking, it's about clear distinctions. – Sulthan Jul 02 '19 at 10:31
  • @Sulthan You're totally right, the difference is SwiftUI enforces it, as it does not make the disable status available. – kontiki Jul 02 '19 at 10:33
  • 1
    Then how does the Apple provided `PlainButtonStyle` know the state of the button? – hasen Dec 04 '19 at 05:50
  • If I'm making my own control, I need to know if it's disabled so I can present it as such. That's not deviating from the source of truth; that's necessary information. – Ky - Jun 17 '20 at 15:33
  • You should ask what data disables the button and tie that data to any other place that needs it. A view is cosmetic the data is where the truth should come from. – lorem ipsum Oct 01 '22 at 11:27
  • Then how would we create a new ToggleSwitchStyle for instance, and not know how the UI should react to `disable`? This makes no sense. – Frederic Adda Jun 04 '23 at 08:06
0

As mentioned by other developers, the main idea of SwiftUI is that the UI remains synced with the data. You can perform this in many different ways. This includes @State, @EnvironmentObject, @Binding etc.

struct ContentView: View {
    
    @State private var isEnabled: Bool = false
    
    var body: some View {
        VStack {
            Button("Press me!") {
                
            }.disabled(isEnabled)
        }
        .padding()
    }
}
azamsharp
  • 19,710
  • 36
  • 144
  • 222
-1

Short answer: Just use inside struct:

@Environment(\.isEnabled) private var isEnabled

Button style with:

  • animation on hover change
  • animation on disable/enable change
  • can be applied on any button in native way of swiftUI
  • you need manually set size of buttons outside of the button

usage:

@State var isDisabled = false

///.......

Button("Styled button") { isDisabled.toggle() }
    .buttonStyle(ButtStyle.BigButton()) // magic inside
    .frame(width: 200, height: 50)
    .disabled(isDisabled)

Button("switch isDisabled") { isDisabled.toggle() }

source code:

public struct ButtStyle { }

// Added style to easy stylyng in native way for SwiftUI
@available(macOS 11.0, *)
public extension ButtStyle {
    struct BigButton: ButtonStyle {
        init() {
            
        }
        
        public func makeBody(configuration: Configuration) -> some View {
            BigButtonStyleView(configuration: configuration)
        }
    }
}

@available(macOS 11.0, *)
struct BigButtonStyleView : View {
    let configuration: ButtonStyle.Configuration
    @Environment(\.isEnabled) var isEnabled // here we getting "disabled"
    
    @State var hover : Bool = false
    
    var body: some View {
        // added animations
        MainFrameMod()
            .animation(.easeInOut(duration: 0.2), value: hover)
            .animation(.easeInOut(duration: 0.2), value: isEnabled)
    }
    
    // added opacity on move hover change
    // and disabled status
    @ViewBuilder
    func MainFrameMod() -> some View {
        if isEnabled {
            MainFrame()
                .opacity(hover ? 1 : 0.8)
                .onHover{ hover = $0 }
        } else {
            MainFrame()
                .opacity(0.5)
        }
    }
    
    // Main interface of button
    func MainFrame() -> some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(Color(hex: 0xD8D8D8))
                
            configuration.label
                .foregroundColor(.black)
                .font(.custom("SF Pro", size: 18))
        }
    }
}
Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101