Bedrock/Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md
Matt Bruce cc650c68d4 Add settings theming guide and fix Color typealias conflicts
- Add SETTINGS_GUIDE.md with comprehensive documentation for creating branded settings screens
- Rename Color.Text to Color.TextColors to avoid conflict with SwiftUI Text view
- Rename Color.Button to Color.ButtonColors to avoid conflict with SwiftUI Button view
- Add accentColor parameter to SettingsSectionHeader for customizable section icons
- Update README and PulsingModifier to use new TextColors typealias
2026-01-04 16:54:04 -06:00

12 KiB

Settings View Setup Guide

This guide explains how to create a branded settings screen using Bedrock's theming system and settings components.

Overview

Bedrock provides:

  1. Color protocols for consistent theming (SurfaceColorProvider, AccentColorProvider, etc.)
  2. Reusable settings components (SettingsToggle, SettingsCard, SegmentedPicker, etc.)
  3. Design constants for spacing, typography, and animations

By creating a custom theme, your app gets a unique visual identity while reusing all the settings UI components.


Step 1: Create Your App's Theme

Create a file called [AppName]Theme.swift in your app's Shared/ folder.

Define Surface Colors

Surface colors create visual depth and separation. Use a subtle tint that matches your brand:

import SwiftUI
import Bedrock

/// Surface colors with a subtle [brand]-tint.
public enum MyAppSurfaceColors: SurfaceColorProvider {
    /// Primary background - darkest level
    public static let primary = Color(red: 0.08, green: 0.06, blue: 0.10)
    
    /// Secondary/elevated surface
    public static let secondary = Color(red: 0.12, green: 0.08, blue: 0.14)
    
    /// Tertiary/card surface - most elevated
    public static let tertiary = Color(red: 0.16, green: 0.11, blue: 0.18)
    
    /// Overlay background (for sheets/modals)
    public static let overlay = Color(red: 0.10, green: 0.07, blue: 0.12)
    
    /// Card/grouped element background
    public static let card = Color(red: 0.14, green: 0.10, blue: 0.16)
    
    /// Subtle fill for grouped content sections
    public static let groupedFill = Color(red: 0.12, green: 0.09, blue: 0.14)
    
    /// Section fill for list sections
    public static let sectionFill = Color(red: 0.16, green: 0.12, blue: 0.18)
}

Define Accent Colors

These are your brand's primary interactive colors:

public enum MyAppAccentColors: AccentColorProvider {
    /// Primary accent - your main brand color
    public static let primary = Color(red: 0.85, green: 0.25, blue: 0.45)
    
    /// Light variant
    public static let light = Color(red: 0.95, green: 0.45, blue: 0.60)
    
    /// Dark variant
    public static let dark = Color(red: 0.65, green: 0.18, blue: 0.35)
    
    /// Secondary accent for contrast
    public static let secondary = Color(red: 1.0, green: 0.95, blue: 0.90)
}

Define Other Color Providers

Complete the theme with text, button, status, border, and interactive colors:

public enum MyAppTextColors: TextColorProvider {
    public static let primary = Color.white
    public static let secondary = Color.white.opacity(Design.Opacity.accent)
    public static let tertiary = Color.white.opacity(Design.Opacity.medium)
    public static let disabled = Color.white.opacity(Design.Opacity.light)
    public static let placeholder = Color.white.opacity(Design.Opacity.overlay)
    public static let inverse = Color.black
}

public enum MyAppButtonColors: ButtonColorProvider {
    public static let primaryLight = Color(red: 0.95, green: 0.40, blue: 0.55)
    public static let primaryDark = Color(red: 0.75, green: 0.20, blue: 0.40)
    public static let secondary = Color.white.opacity(Design.Opacity.subtle)
    public static let destructive = Color.red.opacity(Design.Opacity.heavy)
    public static let cancelText = Color.white.opacity(Design.Opacity.strong)
}

public enum MyAppStatusColors: StatusColorProvider {
    public static let success = Color(red: 0.2, green: 0.8, blue: 0.4)
    public static let warning = Color(red: 1.0, green: 0.75, blue: 0.2)
    public static let error = Color(red: 0.9, green: 0.3, blue: 0.3)
    public static let info = Color(red: 0.5, green: 0.7, blue: 0.95)
}

public enum MyAppBorderColors: BorderColorProvider {
    public static let subtle = Color.white.opacity(Design.Opacity.subtle)
    public static let standard = Color.white.opacity(Design.Opacity.hint)
    public static let emphasized = Color.white.opacity(Design.Opacity.light)
    public static let selected = MyAppAccentColors.primary.opacity(Design.Opacity.medium)
}

public enum MyAppInteractiveColors: InteractiveColorProvider {
    public static let selected = MyAppAccentColors.primary.opacity(Design.Opacity.selection)
    public static let hover = Color.white.opacity(Design.Opacity.subtle)
    public static let pressed = Color.white.opacity(Design.Opacity.hint)
    public static let focus = MyAppAccentColors.light
}

Combine Into a Theme

public enum MyAppTheme: AppColorTheme {
    public typealias Surface = MyAppSurfaceColors
    public typealias Text = MyAppTextColors
    public typealias Accent = MyAppAccentColors
    public typealias Button = MyAppButtonColors
    public typealias Status = MyAppStatusColors
    public typealias Border = MyAppBorderColors
    public typealias Interactive = MyAppInteractiveColors
}

Add Convenience Typealiases

Create top-level typealiases with an App prefix to avoid conflicts with Bedrock's defaults:

/// Short typealiases for cleaner usage throughout the app.
/// These avoid conflicts with Bedrock's default typealiases by using unique names.
///
/// Usage:
/// ```swift
/// .background(AppSurface.primary)
/// .foregroundStyle(AppAccent.primary)
/// ```
typealias AppSurface = MyAppSurfaceColors
typealias AppTextColors = MyAppTextColors
typealias AppAccent = MyAppAccentColors
typealias AppButtonColors = MyAppButtonColors
typealias AppStatus = MyAppStatusColors
typealias AppBorder = MyAppBorderColors
typealias AppInteractive = MyAppInteractiveColors

Important

: Do NOT add typealiases inside extension Color { } as they will conflict with Bedrock's defaults and cause "ambiguous use" compiler errors. Use top-level App-prefixed typealiases instead.


Step 2: Create a Settings Card Container

Add a reusable card container for grouping related settings:

/// A card container that provides visual grouping for settings sections.
struct SettingsCard<Content: View>: View {
    @ViewBuilder let content: Content
    
    var body: some View {
        VStack(alignment: .leading, spacing: Design.Spacing.medium) {
            content
        }
        .padding(Design.Spacing.medium)
        .background(
            RoundedRectangle(cornerRadius: Design.CornerRadius.large)
                .fill(AppSurface.card)
        )
        .overlay(
            RoundedRectangle(cornerRadius: Design.CornerRadius.large)
                .strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
        )
    }
}

Step 3: Build Your Settings View

Use Bedrock's components with your theme colors:

import SwiftUI
import Bedrock

struct SettingsView: View {
    @Bindable var viewModel: SettingsViewModel
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: Design.Spacing.medium) {
                    
                    // Section with header and card
                    SettingsSectionHeader(
                        title: "Appearance",
                        systemImage: "paintbrush",
                        accentColor: AppAccent.primary
                    )
                    
                    SettingsCard {
                        SettingsToggle(
                            title: "Dark Mode",
                            subtitle: "Use dark appearance",
                            isOn: $viewModel.darkMode,
                            accentColor: AppAccent.primary
                        )
                        
                        SegmentedPicker(
                            title: "Theme",
                            options: [("Light", "light"), ("Dark", "dark"), ("System", "system")],
                            selection: $viewModel.theme,
                            accentColor: AppAccent.primary
                        )
                    }
                    
                    // Another section
                    SettingsSectionHeader(
                        title: "Notifications",
                        systemImage: "bell",
                        accentColor: AppAccent.primary
                    )
                    
                    SettingsCard {
                        SettingsToggle(
                            title: "Push Notifications",
                            subtitle: "Receive alerts and updates",
                            isOn: $viewModel.pushEnabled,
                            accentColor: AppAccent.primary
                        )
                    }
                    
                    Spacer(minLength: Design.Spacing.xxxLarge)
                }
                .padding(.horizontal, Design.Spacing.large)
            }
            .background(AppSurface.primary)
            .navigationTitle("Settings")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Done") { dismiss() }
                        .foregroundStyle(AppAccent.primary)
                }
            }
        }
    }
}

Available Components

SettingsSectionHeader

A section header with optional icon and accent color.

SettingsSectionHeader(
    title: "Account",
    systemImage: "person.circle",
    accentColor: Color.Accent.primary
)

SettingsToggle

A toggle row with title and subtitle.

SettingsToggle(
    title: "Sound Effects",
    subtitle: "Play sounds for events",
    isOn: $viewModel.soundEnabled,
    accentColor: Color.Accent.primary
)

SegmentedPicker

A horizontal capsule-style picker.

SegmentedPicker(
    title: "Quality",
    options: [("Low", 0), ("Medium", 1), ("High", 2)],
    selection: $viewModel.quality,
    accentColor: Color.Accent.primary
)

SelectableRow

A card-like row for option selection.

SelectableRow(
    title: "Premium",
    subtitle: "Unlock all features",
    isSelected: plan == .premium,
    accentColor: Color.Accent.primary,
    badge: { BadgePill(text: "$9.99") }
) {
    plan = .premium
}

VolumePicker

A slider with speaker icons for volume/percentage values.

VolumePicker(
    label: "Volume",
    volume: $viewModel.volume,
    accentColor: Color.Accent.primary
)

SettingsRow

A navigation-style row with icon and chevron.

SettingsRow(
    systemImage: "star.fill",
    title: "Rate App",
    iconColor: Color.Status.warning
) {
    openAppStore()
}

BadgePill

A capsule badge for values or tags.

BadgePill(
    text: "$4.99",
    isSelected: isCurrentPlan,
    accentColor: Color.Accent.primary
)

Color Relationship Guide

Surface Level Use Case Visual Depth
AppSurface.primary Main background Darkest
AppSurface.overlay Sheet/modal backgrounds Slightly elevated
AppSurface.card Settings cards Distinct from background
AppSurface.sectionFill Section containers Most elevated
Accent Purpose Color
Interactive elements AppAccent.primary
Highlights AppAccent.light
Pressed states AppAccent.dark
Pro/Premium sections AppStatus.warning
Debug/Error sections AppStatus.error

Tips

  1. Derive surface colors from your brand: Add a subtle RGB tint (e.g., if brand is pink, surfaces should have a rose undertone).

  2. Use consistent accent colors: Pass accentColor: AppAccent.primary to all Bedrock components.

  3. Group related settings: Wrap related toggles/pickers in a SettingsCard for visual hierarchy.

  4. Section-specific accents: Use AppStatus.warning for premium sections, AppStatus.error for debug.

  5. Test with Dynamic Type: Bedrock uses Design.BaseFontSize values that scale properly.

  6. Avoid Color. typealiases: Use App-prefixed typealiases to prevent conflicts with Bedrock's defaults.


Example Apps

  • SelfieCam: Rose/magenta theme with camera-focused settings
  • SelfieRingLight: Similar structure with ring light controls
  • CameraTester: Neutral theme for testing/debugging

See each app's [AppName]Theme.swift for implementation examples.