admin管理员组文章数量:1122832
I have an app that allows the user to personalize the interface with a choice of theme colors. Selecting a color changes the app theme, allowing various elements to be colored accordingly (backgrounds, buttons, icons, etc.)
Until recently, I've resorted to a custom init in the main view to style buttons in a confirmation dialog that acts as primary action menu for the app.
Lately, I've noticed that the buttons in the confirmation dialog no longer respect the user selection of theme color (green in this example), even though no code changes have been made in this regard. Instead of showing the selected theme color, they show the default accent color. I suspect it's since the iOS 18 update or 18.1:
I tried using .tint
and .foregroundStyle
inside the dialog, outside the dialog, everything I could think of, but no luck.
Eventually, I came across .accentColor
which does work, but only if applied in a higher wrapper view. However, it looks like it's deprecated in favor for .tint
, which doesn't work.
So the question: is there any better and more future-proof way to style those confirmation dialog buttons, other than the deprecated .accentColor
?
Here's the code reproducing the issue as seen in the screenshots. To see the dialog buttons in green, uncomment the .accentColor
line:
import SwiftUI
struct ConfirmationDialogButtons: View {
//State values
@State private var showTintDialog = false
@State private var showAccentDialog = false
//Initializer
init(){
UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = UIColor(.green)
}
//Bodyjee
var body: some View {
VStack {
Button("Open tint dialog") {
showTintDialog.toggle()
}
.tint(.blue)
Button("Open regular dialog") {
showAccentDialog.toggle()
}
}
.buttonStyle(.borderedProminent)
.confirmationDialog("This dialog has buttons with green tint modifier", isPresented: $showTintDialog, titleVisibility: .visible) {
Group {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3") {}
}
.tint(.green) // <- this modifier has no effect on buttons in confirmation dialog
.foregroundStyle(.green) // <- this modifier has no effect on buttons in confirmation dialog
}
.confirmationDialog("Select an action", isPresented: $showAccentDialog, titleVisibility: .visible) {
VStack {
Button("Action A") {}
Button("Action B") {}
Button("Action C") {}
}
}
.tint(.green) // <- this modifier has no effect on buttons in confirmation dialog
// .accentColor(.green) // <- THIS WORKS, BUT IT'S DEPRECATED - UNCOMMENT TO COLOR BUTTONS IN CONFIRMATION DIALOG
}
}
#Preview {
ConfirmationDialogButtons()
}
I have an app that allows the user to personalize the interface with a choice of theme colors. Selecting a color changes the app theme, allowing various elements to be colored accordingly (backgrounds, buttons, icons, etc.)
Until recently, I've resorted to a custom init in the main view to style buttons in a confirmation dialog that acts as primary action menu for the app.
Lately, I've noticed that the buttons in the confirmation dialog no longer respect the user selection of theme color (green in this example), even though no code changes have been made in this regard. Instead of showing the selected theme color, they show the default accent color. I suspect it's since the iOS 18 update or 18.1:
I tried using .tint
and .foregroundStyle
inside the dialog, outside the dialog, everything I could think of, but no luck.
Eventually, I came across .accentColor
which does work, but only if applied in a higher wrapper view. However, it looks like it's deprecated in favor for .tint
, which doesn't work.
So the question: is there any better and more future-proof way to style those confirmation dialog buttons, other than the deprecated .accentColor
?
Here's the code reproducing the issue as seen in the screenshots. To see the dialog buttons in green, uncomment the .accentColor
line:
import SwiftUI
struct ConfirmationDialogButtons: View {
//State values
@State private var showTintDialog = false
@State private var showAccentDialog = false
//Initializer
init(){
UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = UIColor(.green)
}
//Bodyjee
var body: some View {
VStack {
Button("Open tint dialog") {
showTintDialog.toggle()
}
.tint(.blue)
Button("Open regular dialog") {
showAccentDialog.toggle()
}
}
.buttonStyle(.borderedProminent)
.confirmationDialog("This dialog has buttons with green tint modifier", isPresented: $showTintDialog, titleVisibility: .visible) {
Group {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3") {}
}
.tint(.green) // <- this modifier has no effect on buttons in confirmation dialog
.foregroundStyle(.green) // <- this modifier has no effect on buttons in confirmation dialog
}
.confirmationDialog("Select an action", isPresented: $showAccentDialog, titleVisibility: .visible) {
VStack {
Button("Action A") {}
Button("Action B") {}
Button("Action C") {}
}
}
.tint(.green) // <- this modifier has no effect on buttons in confirmation dialog
// .accentColor(.green) // <- THIS WORKS, BUT IT'S DEPRECATED - UNCOMMENT TO COLOR BUTTONS IN CONFIRMATION DIALOG
}
}
#Preview {
ConfirmationDialogButtons()
}
Share
Improve this question
asked Nov 23, 2024 at 2:10
Andrei G.Andrei G.
1,0271 gold badge7 silver badges11 bronze badges
3
- If future-proof is your priority, you should make your own confirmation dialog from scratch or use UIKit. – Sweeper Commented Nov 23, 2024 at 7:54
- @Sweeper By future-proof, I merely meant something that won't simply break as soon as iOS 18.2 comes out. – Andrei G. Commented Nov 24, 2024 at 18:58
- Okay, after some reverse-engineering, it seems like SwiftUI deliberately sets the tint colors of the alert actions to the default tint color. So I think the only options are UIKit, or swizzle the method where SwiftUI sets the tint colors. – Sweeper Commented Nov 24, 2024 at 20:58
2 Answers
Reset to default 2The default tint color is the "AccentColor" asset in your asset dialog. So if you just want to have one fixed accent color for all confirmation dialogs, you can just change that.
The confirmation dialog is implemented by a UIAlertController
subclass called PlatformAlertController
. In iOS 18.1, this implementation causes the tint colors of the button labels to be set to the "AccentColor" asset. Because the tint color is deliberately set, the UIView.appearance
trick doesn't work.
This is similar to the behaviour of the accent color of list selection on macOS, which makes me suspect that perhaps in a future version of iOS, users might be able to select the global system tint color, like you can in macOS...
I would suggest just wrapping a view controller that presents a UIAlertController
. Here is an example implementation:
struct UIKitActionSheet: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let title: String?
let message: String?
struct Action {
let title: String?
let style: UIAlertAction.Style
let handler: (() -> Void)?
}
let actions: [Action]
func makeUIViewController(context: Context) -> UIViewController {
context.coordinator.vc
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
context.coordinator.actions = actions
context.coordinator.title = title
context.coordinator.message = message
context.coordinator.onClose = {
isPresented = false
}
if isPresented {
context.coordinator.present()
}
}
func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: UIViewController, context: Context) -> CGSize? {
.zero
}
@MainActor
class Coordinator {
let vc = UIViewController()
var actions: [Action] = []
var title: String?
var message: String?
var onClose: (() -> Void)?
func present() {
guard vc.presentedViewController == nil else { return }
let actionSheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
for action in actions {
actionSheet.addAction(UIAlertAction(title: action.title, style: action.style) { _ in
action.handler?()
self.onClose?()
})
}
vc.present(actionSheet, animated: true, completion: nil)
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
}
// usage example:
@State private var dialog = false
var body: some View {
Button("Present UIKit Action Sheet") {
dialog.toggle()
}
.background {
UIKitActionSheet(isPresented: $dialog, title: "Dialog", message: "Some Message", actions: [
.init(title: "Action 1", style: .default, handler: nil),
.init(title: "Action 2", style: .default, handler: nil),
.init(title: "Action 3", style: .default, handler: nil),
.init(title: "Cancel", style: .cancel, handler: nil),
])
.tint(.red)
}
}
Notably, the UIAlertController
respects the SwiftUI tint
modifier.
A second approach is to swizzle UIAlertController.viewWillAppear
.
extension UIViewController {
@objc func viewWillAppearSwizzled(animated: Bool) {
viewWillAppearSwizzled(animated: animated)
ConfirmationDialogSwizzler.tintLabels(root: view)
}
}
@objc
@MainActor
class ConfirmationDialogSwizzler: NSObject {
static var tint: UIColor = .accent
static var destructiveLabels: Set<String> = []
static func tintLabels(root: UIView) {
if let label = root as? UILabel, !destructiveLabels.contains(label.text ?? "") {
root.tintColor = tint
}
for subview in root.subviews {
tintLabels(root: subview)
}
}
// call this exactly once, at the start of the app
static func swizzleViewWillAppear() {
let originalSelector = #selector(UIAlertController.viewWillAppear)
let swizzledSelector = #selector(UIAlertController.viewWillAppearSwizzled)
let originalMethod = class_getInstanceMethod(UIAlertController.self, originalSelector)!
let swizzledMethod = class_getInstanceMethod(UIAlertController.self, swizzledSelector)!
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
// usage example
@State private var dialog = false
var body: some View {
Button("Present SwiftUI swizzled confirmation dialog") {
ConfirmationDialogSwizzler.tint = .systemGreen
ConfirmationDialogSwizzler.destructiveLabels = ["Action 3"]
dialog.toggle()
}
.confirmationDialog("Dialog", isPresented: $dialog, titleVisibility: .visible) {
Button("Action 1") {}
Button("Action 2") {}
Button("Action 3", role: .destructive) {}
}
.onAppear {
ConfirmationDialogSwizzler.swizzleViewWillAppear()
}
}
To use this, you need to set ConfirmationDialogSwizzler.tint
to the tint color you want before presenting the confirmation dialog. .destructive
buttons will be tinted to that color as well, so I included a destructiveLabels
property to allow you to specify which labels to not tint.
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 button spacing
- The button corner radius
- The button background color
The custom dialog still:
- Allows for setting a dialog title
- Respects the system color scheme (light/dark)
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()
}
Customization examples
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") {}
}
本文标签: swiftuiHow to style buttons in confirmation dialog in iOS 18Stack Overflow
版权声明:本文标题:swiftui - How to style buttons in confirmation dialog in iOS 18+? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736300036a1930673.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论