Reputation: 1
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?
Upvotes: 7
Views: 7234
Reputation: 11140
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:
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
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
.
You can create a dynamic color by creating a Color Set in an asset catalog. It looks like this:
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
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