iosswiftswiftui

How to change SwiftUI alert color scheme?


I have implemented a functionality that changes the color scheme of the app. Everything works fine except the color scheme for the .alert() that has a TextField in it.

If is daylight and color scheme is changed to .dark, the .alert() color scheme is still .light includind the TextField.

That means that when you type something in the TextField, the foreground color is white, because the text color in dark mode changes to white, and you can't really see what you are typing.

Similar, if the system's color scheme is .dark and the app's color scheme is white, the .alert is in .dark mode and the text is black, because the text in the app is black.

Applying .background() or foregroundColor() to TextField or VStack doesn't have any effect.

Is there a way to control the color scheme for .alert()? Or do I have to look for an .alert() alternative?

Here is some code to reproduce the bahaviour. Thanks!

import SwiftUI

struct ContentView: View {
    @State private var showAlert = false
    @State private var addSomething: String = ""
    @State private var isSystemMode = false
    @State private var isDarkMode = false

    var body: some View {
        VStack {
            VStack {
                Toggle("Automatic (iOS Settings)", isOn: $isSystemMode)
                if !isSystemMode {
                    Toggle("Dark Mode", isOn: $isDarkMode)
                }
            }
            Button{
                self.showAlert = true
            } label: {
                Text("Add something")
                    .frame(maxWidth: .infinity)
            }
            .alert("Add something", isPresented: $showAlert, actions: {
                TextField("Something", text: $addSomething)
                Button("Done") {
                    // Some action
                }
                Button("Cancel", role: .cancel, action: {})
            }, message: {
                Text("Type in something")
            })
        }
        .padding()
        .preferredColorScheme(isSystemMode ? .none : isDarkMode ? .dark : .light)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • I think the only way to truly make this work is by making your own custom alert. Here is an example of a custom alert, but you might have to modify it according to your needs:

    struct CustomAlertView<Content: View>: View {
    
        @Environment(\.colorScheme) var colorScheme
    
        let title: String
        let description: String
    
        var cancelAction: (() -> Void)?
        var cancelActionTitle: String?
    
        var primaryAction: (() -> Void)?
        var primaryActionTitle: String?
    
        var customContent: Content?
    
        init(title: String,
             description: String,
             cancelAction: (() -> Void)? = nil,
             cancelActionTitle: String? = nil,
             primaryAction: (() -> Void)? = nil,
             primaryActionTitle: String? = nil,
             customContent: Content? = EmptyView()) {
            self.title = title
            self.description = description
            self.cancelAction = cancelAction
            self.cancelActionTitle = cancelActionTitle
            self.primaryAction = primaryAction
            self.primaryActionTitle = primaryActionTitle
            self.customContent = customContent
        }
    
        var body: some View {
            HStack {
                VStack(spacing: 0) {
                    Text(title)
                        .font(.system(size: 16, weight: .semibold, design: .default))
                        .padding(.top)
                        .padding(.bottom, 8)
    
                    Text(description)
                        .font(.system(size: 12, weight: .light, design: .default))
                        .multilineTextAlignment(.center)
                        .padding([.bottom, .trailing, .leading])
    
                    customContent
    
                    Divider()
    
                    HStack {
                        if let cancelAction, let cancelActionTitle {
                            Button { cancelAction() } label: {
                                Text(cancelActionTitle)
                                    .frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
                            }
                        }
    
                        if cancelActionTitle != nil && primaryActionTitle != nil {
                            Divider()
                        }
    
                        if let primaryAction, let primaryActionTitle {
                            Button { primaryAction() } label: {
                                Text("**\(primaryActionTitle)**")
                                    .frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
                            }
                        }
                    }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 50, alignment: .center)
                }
                .frame(minWidth: 0, maxWidth: 400, alignment: .center)
                .background(.ultraThickMaterial)
                .cornerRadius(10)
                .padding([.trailing, .leading], 50)
            }
            .zIndex(1)
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
            .background(
                colorScheme == .dark
                ? Color(red: 0, green: 0, blue: 0, opacity: 0.4)
                : Color(red: 1, green: 1, blue: 1, opacity: 0.4)
            )
        }
    }
    

    Usage:

    struct ContentView: View {
        @State private var showAlert = false
    
        var body: some View {
            ZStack {
                Button("Show alert") {
                    withAnimation {
                        showAlert.toggle()
                    }
                }
                if showAlert {
                    CustomAlertView(
                        title: "Alert title",
                        description: "Description here",
                        cancelAction: {
                            // Cancel action here
                            withAnimation {
                                showAlert.toggle()
                            }
                        },
                        cancelActionTitle: "Cancel",
                        primaryAction: {
                          // Primary action here
                            withAnimation {
                                showAlert.toggle()
                            }
                        },
                        primaryActionTitle: "Action",
                        customContent:
                            Text("Custom content here")
                            .padding([.trailing, .leading, .bottom])
                    )
                }
            }
        }
    }
    

    If you want to include a TextField simply do:

    customContent:
        TextField("Text here", text: $text)
        .textFieldStyle(.roundedBorder)
        .padding([.trailing, .leading, .bottom])
    

    Preview of custom alert