theme-management

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Theme Management — Expert Decisions

主题管理——专业决策指南

Expert decision frameworks for theming choices in SwiftUI. Claude knows Color and colorScheme — this skill provides judgment calls for when custom theming adds value and architecture trade-offs.

SwiftUI中主题选择的专业决策框架。本指南涵盖Color与colorScheme的相关知识,为自定义主题的适用场景及架构取舍提供判断依据。

Decision Trees

决策树

Do You Need Custom Theming?

是否需要自定义主题?

What's your color requirement?
├─ Standard light/dark mode only
│  └─ System colors are sufficient
│     Color(.systemBackground), Color(.label)
├─ Brand colors that differ from system
│  └─ Asset catalog colors (Named Colors)
│     Define light/dark variants in xcassets
├─ User-selectable themes (beyond light/dark)
│  └─ Full ThemeManager with custom palettes
│     User can choose "Ocean", "Forest", etc.
└─ White-label app (different branding per client)
   └─ Remote theme configuration
      Fetch brand colors from server
The trap: Building a ThemeManager for an app that only needs light/dark mode. Asset catalog colors already handle this automatically.
What's your color requirement?
├─ Standard light/dark mode only
│  └─ System colors are sufficient
│     Color(.systemBackground), Color(.label)
├─ Brand colors that differ from system
│  └─ Asset catalog colors (Named Colors)
│     Define light/dark variants in xcassets
├─ User-selectable themes (beyond light/dark)
│  └─ Full ThemeManager with custom palettes
│     User can choose "Ocean", "Forest", etc.
└─ White-label app (different branding per client)
   └─ Remote theme configuration
      Fetch brand colors from server
误区:只为支持明暗模式的应用构建ThemeManager。资源目录颜色(Asset catalog colors)已能自动处理该需求。

Color Token Architecture

颜色令牌架构

How should you organize colors?
├─ Small app (< 20 screens)
│  └─ Direct Color("name") usage
│     Don't over-engineer
├─ Medium app, single brand
│  └─ Color extension with static properties
│     Color.appPrimary, Color.appBackground
├─ Large app, design system
│  └─ Semantic tokens + primitive tokens
│     Primitives: blue500, gray100
│     Semantic: textPrimary → gray900/gray100
└─ Multi-brand/white-label
   └─ Protocol-based themes
      protocol Theme { var primary: Color }
How should you organize colors?
├─ Small app (< 20 screens)
│  └─ Direct Color("name") usage
│     Don't over-engineer
├─ Medium app, single brand
│  └─ Color extension with static properties
│     Color.appPrimary, Color.appBackground
├─ Large app, design system
│  └─ Semantic tokens + primitive tokens
│     Primitives: blue500, gray100
│     Semantic: textPrimary → gray900/gray100
└─ Multi-brand/white-label
   └─ Protocol-based themes
      protocol Theme { var primary: Color }

Theme Switching Strategy

主题切换策略

When should theme changes apply?
├─ Immediate (all screens at once)
│  └─ Environment-based (@Environment(\.colorScheme))
│     System handles propagation
├─ Per-screen animation
│  └─ withAnimation on theme property change
│     Smooth transition within visible content
├─ Custom transition (like morphing)
│  └─ Snapshot + crossfade technique
│     Complex, usually not worth it
└─ App restart required
   └─ For deep UIKit integration
      Sometimes unavoidable with third-party SDKs
When should theme changes apply?
├─ Immediate (all screens at once)
│  └─ Environment-based (@Environment(\.colorScheme))
│     System handles propagation
├─ Per-screen animation
│  └─ withAnimation on theme property change
│     Smooth transition within visible content
├─ Custom transition (like morphing)
│  └─ Snapshot + crossfade technique
│     Complex, usually not worth it
└─ App restart required
   └─ For deep UIKit integration
      Sometimes unavoidable with third-party SDKs

Dark Mode Compliance Level

深色模式合规等级

What's your dark mode strategy?
├─ System automatic (no custom colors)
│  └─ Free dark mode support
│     UIColor semantic colors work automatically
├─ Custom colors with asset catalog
│  └─ Define both appearances per color
│     Xcode handles switching
├─ Programmatic dark variants
│  └─ Use Color.dynamic(light:dark:)
│     More flexible, harder to maintain
└─ Ignoring dark mode
   └─ DON'T — accessibility issue
      Users expect dark mode support

What's your dark mode strategy?
├─ System automatic (no custom colors)
│  └─ Free dark mode support
│     UIColor semantic colors work automatically
├─ Custom colors with asset catalog
│  └─ Define both appearances per color
│     Xcode handles switching
├─ Programmatic dark variants
│  └─ Use Color.dynamic(light:dark:)
│     More flexible, harder to maintain
└─ Ignoring dark mode
   └─ DON'T — accessibility issue
      Users expect dark mode support

NEVER Do

绝对不要做的事

Color Definition

颜色定义

NEVER hardcode colors in views:
swift
// ❌ Doesn't adapt to dark mode
Text("Hello")
    .foregroundColor(Color(red: 0, green: 0, blue: 0))
    .background(Color.white)

// ✅ Adapts automatically
Text("Hello")
    .foregroundColor(Color(.label))
    .background(Color(.systemBackground))

// Or use named colors from asset catalog
Text("Hello")
    .foregroundColor(Color("TextPrimary"))
NEVER use opposite colors for dark mode:
swift
// ❌ Pure black/white has harsh contrast
let background = colorScheme == .dark ? Color.black : Color.white
let text = colorScheme == .dark ? Color.white : Color.black

// ✅ Use elevated surfaces and softer contrasts
let background = Color(.systemBackground)  // Slightly elevated in dark mode
let text = Color(.label)  // Not pure white in dark mode
NEVER check colorScheme when system colors suffice:
swift
// ❌ Unnecessary — system colors do this
@Environment(\.colorScheme) var colorScheme

var textColor: Color {
    colorScheme == .dark ? .white : .black  // Reimplementing Color(.label)!
}

// ✅ Just use the semantic color
.foregroundColor(Color(.label))
绝对不要在视图中硬编码颜色:
swift
// ❌ Doesn't adapt to dark mode
Text("Hello")
    .foregroundColor(Color(red: 0, green: 0, blue: 0))
    .background(Color.white)

// ✅ Adapts automatically
Text("Hello")
    .foregroundColor(Color(.label))
    .background(Color(.systemBackground))

// Or use named colors from asset catalog
Text("Hello")
    .foregroundColor(Color("TextPrimary"))
绝对不要为深色模式使用完全相反的颜色:
swift
// ❌ Pure black/white has harsh contrast
let background = colorScheme == .dark ? Color.black : Color.white
let text = colorScheme == .dark ? Color.white : Color.black

// ✅ Use elevated surfaces and softer contrasts
let background = Color(.systemBackground)  // Slightly elevated in dark mode
let text = Color(.label)  // Not pure white in dark mode
绝对不要在系统颜色已能满足需求时检查colorScheme:
swift
// ❌ Unnecessary — system colors do this
@Environment(\.colorScheme) var colorScheme

var textColor: Color {
    colorScheme == .dark ? .white : .black  // Reimplementing Color(.label)!
}

// ✅ Just use the semantic color
.foregroundColor(Color(.label))

Theme Architecture

主题架构

NEVER store theme in multiple places:
swift
// ❌ State scattered — which is source of truth?
class ThemeManager {
    @Published var isDark = false
}

struct SettingsView: View {
    @AppStorage("isDark") var isDark = false  // Different storage!
}

// ✅ Single source of truth
class ThemeManager: ObservableObject {
    @AppStorage("theme") var theme: Theme = .system  // One place
}
NEVER force override system preference without user consent:
swift
// ❌ Ignores user's system-wide preference
init() {
    UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
}

// ✅ Only override if user explicitly chose in-app
func applyUserPreference(_ theme: Theme) {
    switch theme {
    case .system:
        window?.overrideUserInterfaceStyle = .unspecified  // Respect system
    case .light:
        window?.overrideUserInterfaceStyle = .light
    case .dark:
        window?.overrideUserInterfaceStyle = .dark
    }
}
NEVER forget accessibility contrast requirements:
swift
// ❌ Low contrast — fails WCAG AA
let textColor = Color(white: 0.6)  // On white background = 2.5:1 ratio
let backgroundColor = Color.white

// ✅ Meets WCAG AA (4.5:1 for normal text)
let textColor = Color(.secondaryLabel)  // System ensures compliance
let backgroundColor = Color(.systemBackground)
绝对不要在多个位置存储主题状态:
swift
// ❌ State scattered — which is source of truth?
class ThemeManager {
    @Published var isDark = false
}

struct SettingsView: View {
    @AppStorage("isDark") var isDark = false  // Different storage!
}

// ✅ Single source of truth
class ThemeManager: ObservableObject {
    @AppStorage("theme") var theme: Theme = .system  // One place
}
绝对不要未经用户同意强制覆盖系统偏好设置:
swift
// ❌ Ignores user's system-wide preference
init() {
    UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
}

// ✅ Only override if user explicitly chose in-app
func applyUserPreference(_ theme: Theme) {
    switch theme {
    case .system:
        window?.overrideUserInterfaceStyle = .unspecified  // Respect system
    case .light:
        window?.overrideUserInterfaceStyle = .light
    case .dark:
        window?.overrideUserInterfaceStyle = .dark
    }
}
绝对不要忘记无障碍对比度要求:
swift
// ❌ Low contrast — fails WCAG AA
let textColor = Color(white: 0.6)  // On white background = 2.5:1 ratio
let backgroundColor = Color.white

// ✅ Meets WCAG AA (4.5:1 for normal text)
let textColor = Color(.secondaryLabel)  // System ensures compliance
let backgroundColor = Color(.systemBackground)

Performance

性能优化

NEVER recompute colors on every view update:
swift
// ❌ Creates new color object every render
var body: some View {
    Text("Hello")
        .foregroundColor(Color(UIColor { traits in
            traits.userInterfaceStyle == .dark ? .white : .black
        }))  // New UIColor closure every time!
}

// ✅ Define once, reuse
extension Color {
    static let adaptiveText = Color(UIColor { traits in
        traits.userInterfaceStyle == .dark ? .white : .black
    })
}

var body: some View {
    Text("Hello")
        .foregroundColor(.adaptiveText)
}

绝对不要在每次视图更新时重新计算颜色:
swift
// ❌ Creates new color object every render
var body: some View {
    Text("Hello")
        .foregroundColor(Color(UIColor { traits in
            traits.userInterfaceStyle == .dark ? .white : .black
        }))  // New UIColor closure every time!
}

// ✅ Define once, reuse
extension Color {
    static let adaptiveText = Color(UIColor { traits in
        traits.userInterfaceStyle == .dark ? .white : .black
    })
}

var body: some View {
    Text("Hello")
        .foregroundColor(.adaptiveText)
}

Essential Patterns

核心模式

Semantic Color System

语义化颜色系统

swift
// Primitive tokens (raw values)
extension Color {
    enum Primitive {
        static let blue500 = Color(hex: "#007AFF")
        static let blue600 = Color(hex: "#0056B3")
        static let gray900 = Color(hex: "#1A1A1A")
        static let gray100 = Color(hex: "#F5F5F5")
    }
}

// Semantic tokens (usage-based)
extension Color {
    static let textPrimary = Color("TextPrimary")  // gray900 light, gray100 dark
    static let textSecondary = Color("TextSecondary")
    static let surfacePrimary = Color("SurfacePrimary")
    static let surfaceSecondary = Color("SurfaceSecondary")
    static let interactive = Color("Interactive")  // blue500
    static let interactivePressed = Color("InteractivePressed")  // blue600

    // Feedback colors (always same hue, adjusted for mode)
    static let success = Color("Success")
    static let warning = Color("Warning")
    static let error = Color("Error")
}
swift
// Primitive tokens (raw values)
extension Color {
    enum Primitive {
        static let blue500 = Color(hex: "#007AFF")
        static let blue600 = Color(hex: "#0056B3")
        static let gray900 = Color(hex: "#1A1A1A")
        static let gray100 = Color(hex: "#F5F5F5")
    }
}

// Semantic tokens (usage-based)
extension Color {
    static let textPrimary = Color("TextPrimary")  // gray900 light, gray100 dark
    static let textSecondary = Color("TextSecondary")
    static let surfacePrimary = Color("SurfacePrimary")
    static let surfaceSecondary = Color("SurfaceSecondary")
    static let interactive = Color("Interactive")  // blue500
    static let interactivePressed = Color("InteractivePressed")  // blue600

    // Feedback colors (always same hue, adjusted for mode)
    static let success = Color("Success")
    static let warning = Color("Warning")
    static let error = Color("Error")
}

ThemeManager with Persistence

带持久化的ThemeManager

swift
@MainActor
final class ThemeManager: ObservableObject {
    static let shared = ThemeManager()

    @AppStorage("selectedTheme") private var storedTheme: String = Theme.system.rawValue

    @Published private(set) var currentTheme: Theme = .system

    enum Theme: String, CaseIterable {
        case system, light, dark

        var overrideStyle: UIUserInterfaceStyle {
            switch self {
            case .system: return .unspecified
            case .light: return .light
            case .dark: return .dark
            }
        }
    }

    private init() {
        currentTheme = Theme(rawValue: storedTheme) ?? .system
        applyTheme(currentTheme)
    }

    func setTheme(_ theme: Theme) {
        currentTheme = theme
        storedTheme = theme.rawValue
        applyTheme(theme)
    }

    private func applyTheme(_ theme: Theme) {
        // Apply to all windows (handles multiple scenes)
        for scene in UIApplication.shared.connectedScenes {
            guard let windowScene = scene as? UIWindowScene else { continue }
            for window in windowScene.windows {
                window.overrideUserInterfaceStyle = theme.overrideStyle
            }
        }
    }
}
swift
@MainActor
final class ThemeManager: ObservableObject {
    static let shared = ThemeManager()

    @AppStorage("selectedTheme") private var storedTheme: String = Theme.system.rawValue

    @Published private(set) var currentTheme: Theme = .system

    enum Theme: String, CaseIterable {
        case system, light, dark

        var overrideStyle: UIUserInterfaceStyle {
            switch self {
            case .system: return .unspecified
            case .light: return .light
            case .dark: return .dark
            }
        }
    }

    private init() {
        currentTheme = Theme(rawValue: storedTheme) ?? .system
        applyTheme(currentTheme)
    }

    func setTheme(_ theme: Theme) {
        currentTheme = theme
        storedTheme = theme.rawValue
        applyTheme(theme)
    }

    private func applyTheme(_ theme: Theme) {
        // Apply to all windows (handles multiple scenes)
        for scene in UIApplication.shared.connectedScenes {
            guard let windowScene = scene as? UIWindowScene else { continue }
            for window in windowScene.windows {
                window.overrideUserInterfaceStyle = theme.overrideStyle
            }
        }
    }
}

Contrast Validation

对比度验证

swift
extension Color {
    func contrastRatio(against background: Color) -> Double {
        let fgLuminance = relativeLuminance
        let bgLuminance = background.relativeLuminance

        let lighter = max(fgLuminance, bgLuminance)
        let darker = min(fgLuminance, bgLuminance)

        return (lighter + 0.05) / (darker + 0.05)
    }

    var meetsWCAGAA: Bool {
        // Check against both light and dark backgrounds
        let lightBg = Color.white
        let darkBg = Color.black
        return contrastRatio(against: lightBg) >= 4.5 ||
               contrastRatio(against: darkBg) >= 4.5
    }

    private var relativeLuminance: Double {
        guard let components = UIColor(self).cgColor.components,
              components.count >= 3 else { return 0 }

        func linearize(_ value: CGFloat) -> Double {
            let v = Double(value)
            return v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4)
        }

        return 0.2126 * linearize(components[0]) +
               0.7152 * linearize(components[1]) +
               0.0722 * linearize(components[2])
    }
}

swift
extension Color {
    func contrastRatio(against background: Color) -> Double {
        let fgLuminance = relativeLuminance
        let bgLuminance = background.relativeLuminance

        let lighter = max(fgLuminance, bgLuminance)
        let darker = min(fgLuminance, bgLuminance)

        return (lighter + 0.05) / (darker + 0.05)
    }

    var meetsWCAGAA: Bool {
        // Check against both light and dark backgrounds
        let lightBg = Color.white
        let darkBg = Color.black
        return contrastRatio(against: lightBg) >= 4.5 ||
               contrastRatio(against: darkBg) >= 4.5
    }

    private var relativeLuminance: Double {
        guard let components = UIColor(self).cgColor.components,
              components.count >= 3 else { return 0 }

        func linearize(_ value: CGFloat) -> Double {
            let v = Double(value)
            return v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4)
        }

        return 0.2126 * linearize(components[0]) +
               0.7152 * linearize(components[1]) +
               0.0722 * linearize(components[2])
    }
}

Quick Reference

快速参考

When to Use Custom Theming

自定义主题的适用场景

ScenarioSolution
Standard light/darkSystem colors only
Brand colorsAsset catalog named colors
User-selectable themesThemeManager + color palettes
White-labelRemote config + theme protocol
场景解决方案
标准明暗模式仅使用系统颜色
品牌专属颜色资源目录命名颜色
用户可选主题ThemeManager + 颜色调色板
白标应用远程配置 + 主题协议

System vs Custom Colors

系统颜色 vs 自定义颜色

NeedSystemCustom
Text colorsColor(.label), Color(.secondaryLabel)Only if brand requires
BackgroundsColor(.systemBackground)Only if brand requires
DividersColor(.separator)Rarely
Tint/accentColor.accentColorUsually brand color
需求系统颜色自定义颜色
文本颜色Color(.label), Color(.secondaryLabel)仅品牌有特殊要求时使用
背景色Color(.systemBackground)仅品牌有特殊要求时使用
分割线Color(.separator)极少使用
强调色Color.accentColor通常为品牌颜色

Contrast Ratios (WCAG)

对比度比值(WCAG标准)

LevelNormal TextLarge TextUse Case
AA4.5:13:1Minimum acceptable
AAA7:14.5:1Enhanced readability
等级常规文本大文本使用场景
AA4.5:13:1最低可接受标准
AAA7:14.5:1增强可读性标准

Red Flags

风险信号

SmellProblemFix
Color(red:green:blue:) in viewNo dark modeUse semantic colors
@Environment colorScheme everywhereOver-engineeringSystem colors adapt automatically
Pure black/whiteHarsh contrastElevated surfaces
Theme in multiple @AppStorageSplit source of truthSingle ThemeManager
window.overrideUserInterfaceStyle in initIgnores user prefOnly on explicit user action
问题迹象潜在问题修复方案
视图中使用Color(red:green:blue:)不支持深色模式使用语义化颜色
随处使用@Environment colorScheme过度设计系统颜色可自动适配
使用纯黑/纯白对比度过于刺眼使用层级化界面
主题存储在多个@AppStorage中数据源不一致使用单一ThemeManager
在init中设置window.overrideUserInterfaceStyle忽略用户偏好仅在用户明确操作时修改