Skip to content

Josscii/SwiftUI_ThemeColor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

Managing Theme Colors in SwiftUI

I always feel compelled to provide a feature to change the theme color in my apps. In SwiftUI, this is quite simple, but there are a few key points to keep in mind.

Setting Theme Colors for a View

You can use the tint modifier to set a theme color for a View and its subviews, like this:

.tint(.red)

Generally, you only need to apply this modifier to the top-level View. Components like Buttons, which have built-in theme colors, will change according to the theme color you set.

Accessing the Set Theme Color within a View

If you want to access the theme color you set, there are two ways to do so.

One way is through Color.accentColor, which can directly fetch this value:

.foregroundStyle(Color.accentColor)

⚠️ Through testing, accentColor can sometimes be unreliable for unknown reasons. There is a workaround for this later on.

Another way is using .tint with modifiers that support ShapeStyle:

.foregroundStyle(.tint)
.background(.tint)
.fill(.tint)
.storke(.tint)

Setting Theme Colors for System Components

Some system components do not follow the tint color setting, such as Alert and FileImporter. In these cases, you can use UIView.appearance to set the theme color, like this:

UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = UIColor(tintColor)
UIView.appearance(whenContainedInInstancesOf: [UIDocumentPickerViewController.self]).tintColor = UIColor(tintColor)

Bringing It All Together

Below is a ViewModifier I wrote to set a unified theme color for all Views in the app. It also includes settings related to colorScheme.

Notably, I added a tintColor environment value to help resolve the issue where Color.accentColor sometimes does not work. You can directly access the set theme color via this value.

private struct TintColor: EnvironmentKey {
    static let defaultValue: Color = .blue
}

extension EnvironmentValues {
    var tintColor: Color {
        get { self[TintColor.self] }
        set { self[TintColor.self] = newValue }
    }
}

struct ThemeModifier: ViewModifier {
    @Default(.customColorScheme) var customColorScheme
    @Default(.customTintColor) var customTintColor
    @Environment(\.colorScheme) private var colorScheme

    var tintColor: Color {
        if let themeColor = ThemeColor(rawValue: customTintColor) {
            return themeColor.color
        } else if let uiColor = UIColor(hexString: customTintColor) {
            return Color(uiColor: uiColor)
        }
        return .indigo
    }

    var preferredColorScheme: ColorScheme {
        customColorScheme.preferredColorScheme ?? colorScheme
    }

    func body(content: Content) -> some View {
        content
            .preferredColorScheme(customColorScheme.preferredColorScheme)
            .tint(tintColor)
            .environment(\.tintColor, tintColor)
            .onAppear {
                updateColorScheme(colorScheme: preferredColorScheme)
                updateTintColor(tintColor: tintColor)
            }
            .onChange(of: preferredColorScheme) {
                updateColorScheme(colorScheme: $0)
            }
            .onChange(of: tintColor) {
                updateTintColor(tintColor: $0)
            }
    }

    func updateColorScheme(colorScheme: ColorScheme) {
        if colorScheme == .dark {
            UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).overrideUserInterfaceStyle = .dark
            UIView.appearance(whenContainedInInstancesOf: [UIDocumentPickerViewController.self]).overrideUserInterfaceStyle = .dark
        } else {
            UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).overrideUserInterfaceStyle = .light
            UIView.appearance(whenContainedInInstancesOf: [UIDocumentPickerViewController.self]).overrideUserInterfaceStyle = .light
        }
    }

    func updateTintColor(tintColor: Color) {
        UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = UIColor(tintColor)
        UIView.appearance(whenContainedInInstancesOf: [UIDocumentPickerViewController.self]).tintColor = UIColor(tintColor)
    }
}

extension View {
    func useTheme() -> some View {
        modifier(ThemeModifier())
    }
}

@main
struct DemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .useTheme()
        }
    }
}

In the code, I used Defaults to simplify the operation of UserDefaults.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published