Search code examples
swiftuiswiftui-button

Reduce text and background opacity when Button is disabled


I am learning SwiftUI and currently building a custom button. What I am trying to achieve is, when the button is disabled its opacity should be 0.5 but not the title. The title opacity should be 0.9 so how can I do this?

This is the button style code:

struct AppButtonStyle: ButtonStyle {
    var backgroundColor: Color
    var textColor: Color
    var isEnabled: Bool

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .background(backgroundColor)
            .foregroundColor(configuration.isPressed ? textColor.opacity(0.2) : textColor)
            .clipShape(Capsule())
            .opacity(isEnabled ? 1.0 : 0.5)
    }
}

and this is the button itself:

struct AppButtonView: View {
    @Binding var isEnabled: Bool
    @State private var title: String

    private var width: CGFloat
    private var height: CGFloat
    private var textColor: Color
    private var backgroundColor: Color
    private var action: () -> Void

    init(title: String,
         width: CGFloat,
         height: CGFloat,
         backgroundColor: Color = AppColors.buttonAlphaBackground.color,
         textColor: Color = AppColors.whiteTextOnly.color,
         isEnabled: Binding<Bool> = .constant(true),
         action: @escaping () -> Void) {
        self._title = State(initialValue: title)
        self.width = width
        self.height = height
        self.backgroundColor = backgroundColor
        self.textColor = textColor
        self._isEnabled = isEnabled
        self.action = action
    }

    var body: some View {
        Button(action: action) {
            Text(title.uppercased())
                .font(Font.custom(AppFonts.onBoardingButton))
                .frame(width: width, height: height)
                .background(.clear)
        }
        .buttonStyle(AppButtonStyle(backgroundColor: backgroundColor, textColor: textColor, isEnabled: isEnabled))
        .disabled(!isEnabled)
    }    
}

On the custom UIKit button view I can easily do this by using nested UIViews:

@IBOutlet weak var iViewBackground: UIView!
@IBOutlet weak var iButtonSubmit: UIButton!

func changeState(enable: Bool) {
    iButtonSubmit.alpha = enable ? 1 : 0.90
    iViewBackground.layer.opacity = enable ? 1 : 0.10
    iButtonSubmit.layer.opacity = enable ? 1 : 0.50
    isEnabled = enable
}

With this approach, only the button's background opacity is set to 0.10, while the title's opacity almost unchanged. In SwiftUI, the entire view including the title is set to 0.10 opacity.

Note: If you have suggestions for a better custom button architecture in SwiftUI, please feel free to share them so I can learn.


Solution

  • From what I can see, you set opacity as the last modifier for the view. Therefore when disabled the modifier reduces opacity of every view that is added by modifiers like background. You can fix it by changing the order of opacity modifiers, so they don't interfere with the background color, like this:

    configuration.label
            .foregroundColor(textColor)
            .opacity(isEnabled ? 1 : 0.9)
            .opacity(configuration.isPressed ? 0.2 : 1)
            .background(backgroundColor.opacity(isEnabled ? 1 : 0.5))
            .clipShape(Capsule())
    

    You can also retrieve foregroundColor through a computed variable to avoid two opacity modifiers (one for pressed state and one for disabled state):

    private func labelOpacity(isEnabled: Bool, isPressed: Bool) -> Double {
        guard isEnabled else { return 0.9 }
        return isPressed ? 0.2 : 1
    }
    
    // then in makeBody
    configuration.label
        .foregroundColor(textColor)
        .opacity(labelOpacity(isEnabled: isEnabled, isPressed: configuration.isPressed)
    

    Also I would recommend to retrieve isEnabled state through Environment in View struct, so you don't have to pass any variables inside (because environment already passes enabled/disabled state):

    @Environment(\.isEnabled) private var isEnabled: Bool
    

    Lastly, title will be updated without the @State property wrapper (because it's created in parent views or observed objects) and you don't need @Binding property wrapper since isEnabled is not updated from within AppButtonView. Basically, with @Environment you can do this on your screens:

    AppButtonView(...).disabled(true)
    

    And it will access isEnabled from the environment itself. I recommend on reading this resource to understand the SwiftUI property wrappers. But keep in mind that title, textColor and all this stuff can be let properties inside of structs because they will be recreated every time parent view updates. And the parent view should be updated when state changes.