swiftPunk
swiftPunk

Reputation: 1

How can I define a custom Primary Color like we get from SwiftUI?

I want define a Color like SwiftUI Primary Color! Right now I am solving my issue with using colorScheme and also using an if like in the code:


struct ContentView: View {
    var body: some View {
        CustomPrimaryView()
            .frame(width: 100, height: 100)
            .cornerRadius(10)
        Color.primary
            .frame(width: 100, height: 100)
            .cornerRadius(10)
    }
}    

struct CustomPrimaryView: View {   
    @Environment(\.colorScheme)
    var colorScheme
    var body: some View {
        colorScheme == .light ?
            Color(red: 0.1, green: 0.1, blue: 0.1) :
            Color(red: 0.95, green: 0.95, blue: 0.95)
    }
}

I would like define a customPrimary Color and access it as Color not as View which dynamically do the job that I am doing in that view. How can we do this in SwiftUI?


. . . also still looking for pure SwiftUI Answer ! ! !


Upvotes: 7

Views: 7234

Answers (3)

Robert Dresler
Robert Dresler

Reputation: 11140

Pure SwiftUI solution

Works as charm for iOS 17+. It also works for iOS 16 and below, you just have to work with it little bit different - see the end of this answer.


My solution works with custom struct called InjectableColor. This struct you can use in models, you can have instances of this struct like InjectableColor.primary etc.


This struct works with custom properties which are resolved in runtime using EnvironmentValues. It kind of changes how you look at the color. Instead of looking at it like it is Color, you look at it like its ShapeStyle, which you can use in foregroundStyle(_:) modifiers and etc.

If you still need to get Color from it, just call getColor(for:) with EnvironmentValues from parent View (see below).


How it looks:

enter image description here


Example of InjectableColor:

import SwiftUI

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
struct InjectableColor: View, ShapeStyle {

    struct ThemeColor {

        let light: Color
        let dark: Color

        func getColor(for environmentValues: EnvironmentValues) -> Color {
            switch environmentValues.colorScheme {
            case .light:
                light
            case .dark:
                dark
            }
        }

    }

    @Environment(\.self) private var environment

    var themeA: ThemeColor
    var themeB: ThemeColor

    var body: SwiftUI.Color {
        getColor(for: environment)
    }

    func resolve(in environment: EnvironmentValues) -> some ShapeStyle {
        getColor(for: environment)
    }

    func getColor(for environmentValues: EnvironmentValues) -> Color {
        switch environmentValues.theme {
        case .a:
            themeA.getColor(for: environmentValues)
        case .b:
            themeB.getColor(for: environmentValues)
        }
    }

}

Theme Environment:

enum Theme {
    case a
    case b
}

private struct ThemeKey: EnvironmentKey {
    static let defaultValue: Theme = .a
}

extension EnvironmentValues {
    var theme: Theme {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

extension View {
    func theme(_ theme: Theme) -> some View {
        environment(\.theme, theme)
    }
}

Usage:

struct InjectableColorPreviewHStack: View {

    @Environment(\.self) private var environment

    static let primaryColor = InjectableColor(
        themeA: InjectableColor.ThemeColor(
            light: Color(red: 0.7, green: 0, blue: 0),
            dark: Color(red: 1, green: 0, blue: 0)
        ),
        themeB: InjectableColor.ThemeColor(
            light: Color(red: 0, green: 0.7, blue: 0),
            dark: Color(red: 0, green: 1, blue: 0)
        )
    )

    var body: some View {
        HStack {
            Self.primaryColor
            Text("text")
                .font(.system(size: 30, weight: .bold))
                .foregroundStyle(Self.primaryColor)
            Circle()
                .fill(Self.primaryColor)
            LinearGradient(
                colors: [
                    Self.primaryColor.getColor(for: environment),
                    Self.primaryColor.getColor(for: environment).opacity(0.5)
                ],
                startPoint: .leading,
                endPoint: .trailing
            )
        }
    }

}

struct InjectableColorPreview: View {

    @Environment(\.self) private var environment

    var body: some View {
        VStack {
            InjectableColorPreviewHStack()
            InjectableColorPreviewHStack().colorScheme(.dark)
            InjectableColorPreviewHStack().theme(.b)
            InjectableColorPreviewHStack().theme(.b).colorScheme(.dark)
        }
    }

}

#Preview {
    InjectableColorPreview()
}


Version compatibility:

This solution works for iOS 17+. But, for iOS 16 and older you can still use it. You just can't use ShapeStyle resolve(in:) methods. So when you want to use it in some modifier as color, you have to resolve it manually by calling getColor(_:) with EnvironmentValues from parent View. For some most common modifiers like foregroundColor you can make custom modifier

struct InjectableColorForegroundColorModifier: ViewModifier {

    @Environment(\.self) private var environment
    private let color: InjectableColor

    init(color: InjectableColor) {
        self.color = color
    }

    func body(content: Content) -> some View {
        content.foregroundColor(color.getColor(for: environment))
    }

}

public extension View {
    func foregroundColor(_ color: InjectableColor) -> some View {
        modifier(InjectableColorForegroundColorModifier(color: color))
    }
}

Upvotes: 3

rob mayoff
rob mayoff

Reputation: 385500

What you're talking about is a dynamic color, which chooses its RGB values at the moment it is applied to fill pixels, based on the current appearance traits.

SwiftUI Color does not (as of 2021) have direct support for creating custom dynamic colors in code. Here are two ways of creating a dynamic Color.

Asset Catalog

You can create a dynamic color by creating a Color Set in an asset catalog. It looks like this:

color set in asset catalog in Xcode

Read more about it in Xcode help: Create asset catalogs and sets.

You can then load the color set as a SwiftUI Color using the Color.init(_ name: String, bundle: Bundle = nil) initializer. For example:

let myColor = Color("MyColor")

UIColor

You can use UIColor.init(dynamicProvider:) to create a dynamic UIColor, then wrap it in a SwiftUI Color:

let uiColor = UIColor { traits in
    if traits.userInterfaceStyle == .dark {
        return .init(red: 0.567, green: 0.622, blue: 0.725, alpha: 1)
    } else {
        return .init(red: 0.204, green: 0.324, blue: 0.374, alpha: 1)
    }
}

let color = Color(uiColor: uiColor)

If you're targeting macOS, you can use NSColor.init(name:dynamicProvider:).

Upvotes: 9

Asperi
Asperi

Reputation: 257493

I assume you wanted something like this

extension Color {
    static var customPrimaryColor: Color {
        let currentStyle = UIScreen.main.traitCollection.userInterfaceStyle
        return currentStyle == .light ? Color(red: 0.1, green: 0.1, blue: 0.1) : Color(red: 0.95, green: 0.95, blue: 0.95)
    }
}

and you can use it directly like

var body: some View {
    Text("HELLO WORLD!")
        .foregroundColor(.customPrimaryColor)
}

and this will be updated automatically on device's color scheme changes

Tested with Xcode 13 / iOS 15

Upvotes: 1

Related Questions