In SwiftUI, customizing your app’s appearance to align with a specific theme can be a powerful tool for enhancing user experience. However, managing these customizations often involves a burdensome repetition of environment values across various views. In this blog post, we’ll explore how to create custom ShapeStyle
implementations that allow you to use your own colors seamlessly throughout your app. This approach simplifies the process by enabling centralized theme management and dynamic color adjustments based on environment values like color schemes and user preferences.
Let’s start with some code. I created a Theme
struct to have some specific colors for my app which I also want to have available through the Environment
.
struct Theme {
var brand: Color
var text: Color
var highlight: Color
}
extension Theme {
static let mainTheme = Theme(brand: .orange, text: .black, highlight: .pink)
}
extension EnvironmentValues {
@Entry var theme: Theme = .mainTheme
}
If we want to use these themed colors in our views, we need to access them through @Environment(\.theme) var theme
.
struct SimpleViewWithEnvironment: View {
@Environment(\.theme) private var theme
var body: some View {
VStack(spacing: 20) {
Text("Hello to my App 👋")
.foregroundStyle(theme.brand)
.font(.largeTitle)
Text("""
Welcome to a whole new experience! Dive into the world of
\(Text("seamless interactions").foregroundStyle(theme.highlight))
and intuitive design. We’re excited to have you on board and can't wait to show you what we've built.
""")
.foregroundStyle(theme.text)
}
.padding()
}
}
If you want to use your own theming in SwiftUI and don’t want to use some static property like Theme.mainTheme
because changing the theme during runtime wouldn’t update the current view. Or you find it tedious to add @Environment(\.theme) var theme
in any View file where you want to access your brand, text etc. colors.
There is a simple way to archive this with a custom ShapeStyle
implementation:
struct ThemeColor: ShapeStyle {
private let keyPath: KeyPath
init(_ keyPath: KeyPath) {
self.keyPath = keyPath
}
func resolve(in environment: EnvironmentValues) -> some ShapeStyle {
let currentTheme = environment.theme
return currentTheme[keyPath: keyPath]
}
}
extension ShapeStyle where Self == ThemeColor {
static var brand: ThemeColor { ThemeColor(\.brand) }
static var text: ThemeColor { ThemeColor(\.text) }
static var highlight: ThemeColor { ThemeColor(\.highlight) }
}
You can use .foregroundStyle
as you would with predefined colors like .foregroundStyle(.red)
just with your themed colors.
And switching theme for a subview or the whole app is as simple as changing any other environment value: .environment(\.theme, Theme(brand: .green, text: .gray, highlight: .teal))
.
struct CustomShapeStyleView: View {
var body: some View {
VStack(spacing: 20) {
Text("Hello to my App 👋")
.foregroundStyle(.brand)
.font(.largeTitle)
Text("""
Welcome to a whole new experience! Dive into the world of \(Text("seamless interactions").foregroundStyle(.highlight)) and intuitive design. We’re excited to have you on board and can't wait to show you what we've built.
""")
.foregroundStyle(.text)
}
.padding()
.environment(\.theme, Theme(brand: .green, text: .gray, highlight: .teal))
}
}
With this, you don’t need to add @Environment(\.theme) var theme
to every view or need to call your theme colors with static properties like .foregroundStyle(ThemeManager.currentTheme.brand)
💡 If you don’t want to recreate your semantic color names in a ShapeStyle extension you can also just use a wrapper function like this:
static func theme(_ keyPath: KeyPath
) -> ThemeColor { ThemeColor(keyPath) } And use it in the View
.foregroundStyle(.theme(\.brand))
Not only for Theming
It’s not only useful when you want to bring your own theming into your app.
Sometimes you need a different color depending on your user’s preferences regarding dark & light modes.With the same approach, you can easily access the current colorScheme
from the Environment.
extension EnvironmentValues {
@Entry var lightTheme: Theme = .mainTheme
@Entry var darkTheme: Theme = Theme(brand: .white, text: .gray, highlight: .cyan)
}
struct ThemeColorScheme: ShapeStyle {
private let keyPath: KeyPath
init(_ keyPath: KeyPath) {
self.keyPath = keyPath
}
func resolve(in environment: EnvironmentValues) -> some ShapeStyle {
if environment.colorScheme == .dark {
return environment.darkTheme[keyPath: keyPath]
} else {
return environment.lightTheme[keyPath: keyPath]
}
}
}
Another solution would be to initialize the theme already with the UIColor(dynamicProvider:) initalizer, but this would force us to UIKit and we need to implement another solution for macOS apps.
Accessibility
Or lets talk about accessibility. Not every culture uses red as a warning, so you could create your own destructive style which returns different colors depending on the region of the user.Or add a different behavior depending on the platform the user is using your app.
struct DestructiveColor: ShapeStyle {
func resolve(in environment: EnvironmentValues) -> some ShapeStyle {
if environment.locale.region?.identifier == "CN" {
Color.orange
} else {
Color.red
}
}
}
extension ShapeStyle where Self == DestructiveColor {
static var destructive: DestructiveColor {
DestructiveColor()
}
}
Which can be used like this:
struct DestructiveColors: View {
@Environment(\.locale) var locale
var body: some View {
VStack {
Button {} label: {
Label("Delete profile", systemImage: "person.crop.circle.badge.exclamationmark")
.padding()
.background(.destructive)
.clipShape(.capsule)
}
Button {} label: {
Label("Delete profile", systemImage: "person.crop.circle.badge.exclamationmark")
.padding()
.background(.destructive)
.clipShape(.capsule)
}
.environment(\.locale, Locale.init(languageCode: .chinese, script: nil, languageRegion: .chinaMainland))
}
.foregroundStyle(.white)
}
}
Conclusion
By implementing custom ShapeStyle
for your app, you can significantly streamline the theming process. This approach not only reduces the repetition of environment value declarations but also centralizes theme management, allowing for real-time adjustments based on user preferences, color schemes, and accessibility requirements. The versatility of custom ShapeStyle extends beyond theming, offering solutions for dynamic appearances in dark/light modes and culturally sensitive designs. Embrace this technique to enhance the user experience and maintain a clean, maintainable codebase. Happy coding!