As per @Sweeper's initial suggestion in the comments, I ended up creating a custom confirmation dialog from scratch, that seeks to emulate the default .confirmationDialog
, with some extra customization options.
With this custom confirmation, I have control over the color of the button labels, plus additional control over:
The custom dialog still:
There may be some other features missing when compared to the default, but for my purposes, this is suitable at this stage.
Here's the full code, followed by some example usage and screenshots:
import SwiftUI
struct ConfirmationDialogButtons: View {
//State values
@State private var showButtonDialog = true
//Body
var body: some View {
ZStack {
VStack {
Button("Open button dialog") {
showButtonDialog.toggle()
}
}
.buttonStyle(.borderedProminent)
.buttonDialog(
title: "Some menu title text",
isPresented: $showButtonDialog,
labelColor: .green
) {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3") {}
}
}
.tint(.green)
}
}
extension View {
func buttonDialog(
title: String = "",
isPresented: Binding<Bool>,
labelColor: Color? = nil,
buttonSpacing: CGFloat? = nil,
buttonBackground: Color? = nil,
buttonCornerRadius: CGFloat? = nil,
@ViewBuilder buttons: @escaping () -> some View
) -> some View {
self
.modifier(
ButtonDialogModifier(
title: title,
isPresented: isPresented,
labelColor: labelColor,
buttonSpacing: buttonSpacing,
buttonBackground: buttonBackground,
buttonCornerRadius: buttonCornerRadius,
buttons: buttons
)
)
}
}
struct ButtonDialogModifier<Buttons: View>: ViewModifier {
//Parameters
var title: String
@Binding var isPresented: Bool
var labelColor: Color
var buttonSpacing: CGFloat
var buttonBackground: Color
var buttonCornerRadius: CGFloat
var dialogCornerRadius: CGFloat
@ViewBuilder let buttons: () -> Buttons
//Default values
private let defaultButtonBackground: Color = Color(UIColor.secondarySystemBackground)
private let defaultCornerRadius: CGFloat = 12
private var cancelButtonLabelColor: Color
//Initializer
init(
title: String? = nil,
isPresented: Binding<Bool>,
labelColor: Color? = nil,
buttonSpacing: CGFloat? = nil,
buttonBackground: Color? = nil,
buttonCornerRadius: CGFloat? = nil,
dialogCornerRadius: CGFloat? = nil,
buttons: @escaping () -> Buttons
) {
//Initialize with default values
self.title = title ?? ""
self._isPresented = isPresented
self.labelColor = labelColor ?? .accentColor
self.buttonSpacing = buttonSpacing ?? 0
self.buttonBackground = buttonBackground ?? defaultButtonBackground
self.buttonCornerRadius = (buttonCornerRadius != nil ? buttonCornerRadius : self.buttonSpacing == 0 ? 0 : buttonCornerRadius) ?? defaultCornerRadius
self.dialogCornerRadius = dialogCornerRadius ?? buttonCornerRadius ?? defaultCornerRadius
self.buttons = buttons
self.cancelButtonLabelColor = self.buttonBackground == defaultButtonBackground ? self.labelColor : self.buttonBackground
}
//Body
func body(content: Content) -> some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay {
ZStack(alignment: .bottom) {
if isPresented {
Color.black
.opacity(0.2)
.ignoresSafeArea()
.transition(.opacity)
}
if isPresented {
//Menu wrapper
VStack(spacing: 10) {
VStack(spacing: buttonSpacing) {
Text(title)
.foregroundStyle(.secondary)
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .center)
.padding()
.background(Color(UIColor.secondarySystemBackground), in: RoundedRectangle(cornerRadius: buttonCornerRadius))
// Apply style for each button passed in content
buttons()
.buttonStyle(FullWidthButtonStyle(labelColor: labelColor, buttonBackground: buttonBackground, buttonCornerRadius: buttonCornerRadius))
}
.font(.title3)
.clipShape(RoundedRectangle(cornerRadius: dialogCornerRadius))
//Cancel button
Button {
isPresented.toggle()
} label: {
Text("Cancel")
.fontWeight(.semibold)
}
.buttonStyle(FullWidthButtonStyle(labelColor: cancelButtonLabelColor, buttonBackground: Color(UIColor.tertiarySystemBackground), buttonCornerRadius: dialogCornerRadius))
}
.font(.title3)
.padding(10)
.transition(.move(edge: .bottom))
}
}
.animation(.easeInOut, value: isPresented)
}
}
//Custom full-width button style
private struct FullWidthButtonStyle: ButtonStyle {
//Parameters
var labelColor: Color
var buttonBackground: Color = Color(UIColor.secondarySystemBackground)
var buttonCornerRadius: CGFloat
//Body
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity) // Make the button full width
.padding()
.background(buttonBackground, in: RoundedRectangle(cornerRadius: buttonCornerRadius))
.opacity(configuration.isPressed ? 0.8 : 1.0) // Add press feedback
.foregroundStyle(labelColor)
.overlay(Divider(), alignment: .top)
}
}
}
#Preview {
ConfirmationDialogButtons()
}
Simple color label customization:
.buttonDialog(
title: "Some menu title text",
isPresented: $showButtonDialog,
labelColor: .cyan // <- Simple button label color customization
) {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3") {}
}
Dark mode support (follow system setting):
Button spacing:
.buttonDialog(
title: "Some menu title text",
isPresented: $showButtonDialog,
labelColor: .cyan, // <- Button label color customization
buttonSpacing: 10 // <- Button spacing
) {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3") {}
}
Button corner radius:
.buttonDialog(
title: "Some menu title text",
isPresented: $showButtonDialog,
labelColor: .cyan, // <- Button label color customization
buttonSpacing: 10, // <- Button spacing
buttonCornerRadius: 30 // <- Button corner radius
) {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3") {}
}
Custom button background:
.buttonDialog(
title: "Some menu title text",
isPresented: $showButtonDialog,
labelColor: .white, // <- Button label color customization
buttonSpacing: 10, // <- Button spacing
buttonBackground: .green, // <- Button background
buttonCornerRadius: 30 // <- Button corner radius
) {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3") {}
}