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.
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.