I use a custom AppTheme type and inject it via a custom EnvironmentKey. Views can access the theme using @Environment(\.appTheme) or override it explicitly.
While this work fine in most cases it somhow causes mysterios crashed deep inside SwiftUI internal code. The crash does not happen inside my code and Xcode does not show a meaningful stack trace.
The crash only happens when using the environment value inside the modifier (see demo code below). Using @Environment(\.appTheme) directly inside views works perfectly.
The theme does not change dynamically and is a plain struct. I only want globally accessible style definitions.
So the question is: Is there anything fundamentally wrong with using a custom theme via @Environment inside a ViewModifier?
I cannot tell whether:
- my general theme architecture is flawed,
- I’m accidentally triggering some SwiftUI limitation regarding Environment inside modifiers,
- or this is a SwiftUI bug.
I’m not trying to implement dynamic theme switching or anything complex — I only want a central place for styles that I can access across the app without manually passing the theme down everywhere.
Before trying to redesign everything, I want to know whether this pattern is supposed to work, or whether SwiftUI has a known limitation here.
Any insights appreciated.
Code:
// Abstract AppTheme build on tokens
struct AppThemeTextTokens {
var primary: Color
var font: Font
}
struct AppTheme {
var text: AppThemeTextTokens
//...
static let `default` = AppTheme(...)
}
// Concrete, app specifix app theme
extension AppTheme {
static let theme = AppTheme(
text: .init(primary: .primary, font: .largeTitle)
)
//...
}
// App Theme EnvironmentKey
private struct AppThemeKey: EnvironmentKey {
static var defaultValue: AppTheme = AppTheme.default
}
extension EnvironmentValues {
var appTheme: AppTheme {
get { self[AppThemeKey.self] }
set { self[AppThemeKey.self] = newValue }
}
}
// Modifier to apply theme to Text views
struct AppThemeTextModifier: ViewModifier {
let theme: AppTheme?
@Environment(\.appTheme) private var envTheme // <-- This somehow causes the crash
func body(content: Content) -> some View {
let theme = theme ?? envTheme // <-- Crash
// let theme = theme ?? PTAppTheme.theme // <-- No Crash
return content
.font(theme.text.font)
.foregroundColor(theme.text.primary)
}
}
extension View {
func textAppTheme(_ theme: AppTheme? = nil) -> some View {
modifier(AppThemeTextModifier(theme: theme))
}
}
// Inject theme to @Environment
struct RootView: View {
let theme = PTAppTheme.theme
var body: some View {
SomeSubViews()
.environment(\.appTheme, theme)
}
}
// Theme usage via @Environment
struct DeepDownSubview: View {
@Environment(\.appTheme) private var theme
var body: some View {
Text("Hello World")
.textAppTheme()
}
}
.sheetor.fullScreenCover, maybe also tips, alerts or popovers) do not inherit the environment of the calling view, environment values have to be passed to these views explicitly. Could this be a possible cause of the problem?@Environment(\.appTheme)is not set, e.g. in a.sheetit should simply default toAppTheme.defaultand not crash.let theme = theme ?? envTheme