Bedrock/Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md

28 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. Ensure ALL typealiases mentioned in the theme are created, as components like SettingsToggle or SettingsCard rely on them for consistent branding.


Step 2: 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(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
                        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(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
                        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

SettingsCard

A card container for visually grouping related settings.

SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
    SettingsToggle(...)
    SettingsSlider(...)
}

Refactoring: Replace inline card styling with SettingsCard:

// ❌ BEFORE: Inline card styling
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)
)

// ✅ AFTER: Use Bedrock component
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
    content
}

SettingsSectionHeader

A section header with optional icon and accent color.

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

SettingsToggle

A toggle row with title, subtitle, and optional title accessory (e.g., premium crown). Note: Subtitle is required.

// Basic toggle
SettingsToggle(
    title: "Sound Effects",
    subtitle: "Play sounds for events",
    isOn: $viewModel.soundEnabled,
    accentColor: AppAccent.primary
)

// Premium toggle with crown icon
SettingsToggle(
    title: "Flash Sync",
    subtitle: "Use ring light color for flash",
    isOn: $viewModel.flashSync,
    accentColor: AppAccent.primary,
    titleAccessory: {
        Image(systemName: "crown.fill")
            .foregroundStyle(AppStatus.warning)
    }
)

Refactoring: Replace inline premium toggles with titleAccessory:

// ❌ BEFORE: Custom premium toggle
Toggle(isOn: $isOn) {
    VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
        HStack(spacing: Design.Spacing.xSmall) {
            Text(title)
                .font(.subheadline.weight(.medium))
                .foregroundStyle(.white)
            
            Image(systemName: "crown.fill")
                .font(.caption2)
                .foregroundStyle(AppStatus.warning)
        }
        
        Text(subtitle)
            .font(.subheadline)
            .foregroundStyle(.white.opacity(Design.Opacity.medium))
    }
}
.tint(AppAccent.primary)

// ✅ AFTER: Use titleAccessory parameter
SettingsToggle(
    title: title,
    subtitle: subtitle,
    isOn: $isOn,
    accentColor: AppAccent.primary,
    titleAccessory: {
        Image(systemName: "crown.fill")
            .foregroundStyle(AppStatus.warning)
    }
)

SettingsSlider

A slider with title, subtitle, value display, and optional icons.

// Basic slider with custom format
SettingsSlider(
    title: "Ring Size",
    subtitle: "Adjusts the size of the ring",
    value: $viewModel.ringSize,
    in: 20...100,
    step: 5,
    format: SliderFormat.integer(unit: "pt"),
    accentColor: AppAccent.primary,
    leadingIcon: Image(systemName: "circle"),
    trailingIcon: Image(systemName: "circle")
)

// Percentage slider
SettingsSlider(
    title: "Brightness",
    subtitle: "Adjusts the brightness",
    value: $viewModel.brightness,
    in: 0.1...1.0,
    step: 0.05,
    format: SliderFormat.percentage,
    accentColor: AppAccent.primary,
    leadingIcon: Image(systemName: "sun.min"),
    trailingIcon: Image(systemName: "sun.max.fill")
)

Format Helpers:

  • SliderFormat.percentage - Shows value as percentage (0.5 → "50%")
  • SliderFormat.integer(unit: "pt") - Shows integer with unit (40 → "40pt")
  • SliderFormat.decimal(precision: 1, unit: "x") - Shows decimal (1.5 → "1.5x")
  • Custom closure: { "\(Int($0))°" } - Any custom format

Refactoring: Replace inline slider implementations:

// ❌ BEFORE: Inline slider with manual layout
VStack(alignment: .leading, spacing: Design.Spacing.small) {
    HStack {
        Text("Ring Size")
            .font(.subheadline.weight(.medium))
            .foregroundStyle(.white)
        Spacer()
        Text("\(Int(viewModel.ringSize))pt")
            .font(.subheadline.weight(.medium))
            .fontDesign(.rounded)
            .foregroundStyle(.white.opacity(Design.Opacity.medium))
    }
    
    Text("Adjusts the size of the ring")
        .font(.caption)
        .foregroundStyle(.white.opacity(Design.Opacity.medium))
    
    HStack(spacing: Design.Spacing.medium) {
        Image(systemName: "circle")
            .font(.caption2)
            .foregroundStyle(.white.opacity(Design.Opacity.medium))
        
        Slider(value: $viewModel.ringSize, in: 20...100, step: 5)
            .tint(AppAccent.primary)
        
        Image(systemName: "circle")
            .font(.callout)
            .foregroundStyle(.white.opacity(Design.Opacity.medium))
    }
}
.padding(.vertical, Design.Spacing.xSmall)

// ✅ AFTER: Use Bedrock component
SettingsSlider(
    title: "Ring Size",
    subtitle: "Adjusts the size of the ring",
    value: $viewModel.ringSize,
    in: 20...100,
    step: 5,
    format: SliderFormat.integer(unit: "pt"),
    accentColor: AppAccent.primary,
    leadingIcon: Image(systemName: "circle"),
    trailingIcon: Image(systemName: "circle")
)

SettingsNavigationRow

A navigation link row for settings that navigate to detail views.

SettingsNavigationRow(
    title: "Open Source Licenses",
    subtitle: "Third-party libraries used in this app",
    backgroundColor: AppSurface.primary
) {
    LicensesView()
}

Refactoring: Replace inline NavigationLink styling:

// ❌ BEFORE: Inline NavigationLink with custom styling
NavigationLink {
    LicensesView()
} label: {
    HStack {
        VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
            Text("Open Source Licenses")
                .font(.subheadline.weight(.medium))
                .foregroundStyle(.white)
            
            Text("Third-party libraries used in this app")
                .font(.caption)
                .foregroundStyle(.white.opacity(Design.Opacity.medium))
        }
        
        Spacer()
        
        Image(systemName: "chevron.right")
            .font(.caption)
            .foregroundStyle(.white.opacity(Design.Opacity.medium))
    }
    .padding(Design.Spacing.medium)
    .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
}
.buttonStyle(.plain)

// ✅ AFTER: Use Bedrock component
SettingsNavigationRow(
    title: "Open Source Licenses",
    subtitle: "Third-party libraries used in this app",
    backgroundColor: AppSurface.primary
) {
    LicensesView()
}

LicensesView

A reusable view for displaying open source licenses used in your app.

Create an app-specific wrapper view with licenses

import SwiftUI
import Bedrock

struct AppLicensesView: View {
    private static let licenses: [License] = [
        License(
            name: "MijickCamera",
            url: "https://github.com/Mijick/Camera",
            licenseType: "Apache 2.0 License",
            description: "Camera framework for SwiftUI."
        ),
        License(
            name: "RevenueCat",
            url: "https://github.com/RevenueCat/purchases-ios",
            licenseType: "MIT License",
            description: "In-app subscriptions made easy."
        )
    ]
    
    var body: some View {
        LicensesView(
            licenses: Self.licenses,
            backgroundColor: AppSurface.overlay,
            cardBackgroundColor: AppSurface.card,
            cardBorderColor: AppBorder.subtle,
            accentColor: AppAccent.primary
        )
    }
}

Navigate to it from settings

SettingsNavigationRow(
    title: "Open Source Licenses",
    subtitle: "Third-party libraries used in this app",
    backgroundColor: .clear
) {
    AppLicensesView()
}

SettingsSegmentedPicker

A segmented picker with title, subtitle, and optional accessory (follows the same pattern as SettingsToggle and SettingsSlider).

// Basic picker
SettingsSegmentedPicker(
    title: "Camera",
    subtitle: "Choose between front and back camera lenses",
    options: [("Front", .front), ("Back", .back)],
    selection: $viewModel.cameraPosition,
    accentColor: AppAccent.primary
)

// Premium picker with crown icon
SettingsSegmentedPicker(
    title: "HDR Mode",
    subtitle: "High Dynamic Range for better lighting in photos",
    options: [("Off", .off), ("On", .on), ("Auto", .auto)],
    selection: $viewModel.hdrMode,
    accentColor: AppAccent.primary,
    titleAccessory: {
        Image(systemName: "crown.fill")
            .foregroundStyle(AppStatus.warning)
    }
)
.disabled(!isPremiumUnlocked)

Refactoring: Replace inline segmented pickers:

// ❌ BEFORE: Inline title/subtitle with SegmentedPicker
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
    Text("Camera")
        .font(.subheadline.weight(.medium))
        .foregroundStyle(.white)
    
    Text("Choose between front and back camera lenses")
        .font(.caption)
        .foregroundStyle(.white.opacity(Design.Opacity.medium))
    
    SegmentedPicker(
        title: "",
        options: [("Front", .front), ("Back", .back)],
        selection: $cameraPosition
    )
}

// ✅ AFTER: Use SettingsSegmentedPicker
SettingsSegmentedPicker(
    title: "Camera",
    subtitle: "Choose between front and back camera lenses",
    options: [("Front", .front), ("Back", .back)],
    selection: $cameraPosition
)

SegmentedPicker (Low-level)

A horizontal capsule-style picker without title/subtitle styling. Use SettingsSegmentedPicker for settings screens.

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

SettingsRow

An action row with icon and chevron (for non-navigation actions).

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

SelectableRow

A card-like row for option selection.

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

BadgePill

A capsule badge for values or tags.

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

iCloud Sync Section

Bedrock provides a reusable iCloud sync section with the CloudSyncable protocol and iCloudSyncSettingsView.

Step 1: Conform ViewModel to CloudSyncable

import Bedrock

@Observable @MainActor
final class SettingsViewModel: CloudSyncable {
    private let cloudSync = CloudSyncManager<SyncedSettings>()
    
    // CloudSyncable protocol requirements
    var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
    var iCloudEnabled: Bool {
        get { cloudSync.iCloudEnabled }
        set { cloudSync.iCloudEnabled = newValue }
    }
    var lastSyncDate: Date? { cloudSync.lastSyncDate }
    var syncStatus: String { cloudSync.syncStatus }
    var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
    
    func forceSync() { cloudSync.sync() }
}

Step 2: Use iCloudSyncSettingsView

SettingsSectionHeader(title: "iCloud Sync", systemImage: "icloud", accentColor: AppAccent.primary)

SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
    iCloudSyncSettingsView(
        viewModel: viewModel,
        accentColor: AppAccent.primary,
        successColor: AppStatus.success,
        warningColor: AppStatus.warning
    )
}

The component automatically handles:

  • Toggle for enabling/disabling sync
  • Dynamic subtitle based on iCloud availability
  • Sync status display with appropriate icons
  • "Sync Now" button
  • Relative time formatting for last sync date

Debug Section Pattern

Add a debug section to settings for developer tools. This section is only visible in DEBUG builds.

Standard Debug Section Structure

// In your SettingsView body, after other sections:

// MARK: - Debug Section

#if DEBUG
SettingsSectionHeader(
    title: "Debug",
    systemImage: "ant.fill",
    accentColor: AppStatus.error  // Red accent for debug
)

SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
    debugSection
}
#endif

Common Debug Section Content

Create a debug section view with common developer tools:

#if DEBUG
private var debugSection: some View {
    VStack(spacing: Design.Spacing.small) {
        // Debug Premium Toggle - unlock features for testing
        SettingsToggle(
            title: "Enable Debug Premium",
            subtitle: "Unlock all premium features for testing",
            isOn: $viewModel.isDebugPremiumEnabled,
            accentColor: AppStatus.warning
        )
        
        // Icon Generator - generate app icons
        SettingsNavigationRow(
            title: "Icon Generator",
            subtitle: "Generate and save app icon to Files",
            backgroundColor: AppSurface.primary
        ) {
            IconGeneratorView(config: .myApp, appName: "MyApp")
        }
        
        // Branding Preview - preview icon and launch screen
        SettingsNavigationRow(
            title: "Branding Preview",
            subtitle: "Preview app icon and launch screen",
            backgroundColor: AppSurface.primary
        ) {
            BrandingPreviewView(
                iconConfig: .myApp,
                launchConfig: .myApp,
                appName: "MyApp"
            )
        }
    }
}
#endif

Available Bedrock Debug/Branding Views

View Purpose Parameters
IconGeneratorView Generate and save app icons config, appName
BrandingPreviewView Preview icon and launch screen iconConfig, launchConfig, appName
AppIconView Render app icon config
LaunchScreenView Render launch screen config

ViewModel Support for Debug Premium

Add a debug premium property to your SettingsViewModel:

#if DEBUG
/// Debug-only: Simulates premium being unlocked for testing
var isDebugPremiumEnabled: Bool {
    get { UserDefaults.standard.bool(forKey: "debugPremiumEnabled") }
    set { UserDefaults.standard.set(newValue, forKey: "debugPremiumEnabled") }
}
#endif

/// Whether premium features are unlocked
var isPremiumUnlocked: Bool {
    #if DEBUG
    if isDebugPremiumEnabled { return true }
    #endif
    return premiumManager.isPremiumUnlocked
}

Refactoring: Extract Inline Debug Sections

// ❌ BEFORE: Inline debug content
#if DEBUG
VStack(spacing: Design.Spacing.small) {
    Toggle(isOn: $viewModel.isDebugPremiumEnabled) {
        VStack(alignment: .leading) {
            Text("Enable Debug Premium")
            Text("Unlock all premium features for testing")
        }
    }
    
    NavigationLink {
        IconGeneratorView(config: .myApp, appName: "MyApp")
    } label: {
        HStack {
            Text("Icon Generator")
            Spacer()
            Image(systemName: "chevron.right")
        }
    }
}
#endif

// ✅ AFTER: Use Bedrock components
#if DEBUG
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error)

SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
    SettingsToggle(
        title: "Enable Debug Premium",
        subtitle: "Unlock all premium features for testing",
        isOn: $viewModel.isDebugPremiumEnabled,
        accentColor: AppStatus.warning
    )
    
    SettingsNavigationRow(
        title: "Icon Generator",
        subtitle: "Generate and save app icon to Files",
        backgroundColor: AppSurface.primary
    ) {
        IconGeneratorView(config: .myApp, appName: "MyApp")
    }
}
#endif

Component Summary Table

Component Use Case Key Parameters
SettingsCard Group related settings backgroundColor, borderColor
SettingsSectionHeader Section titles title, systemImage, accentColor
SettingsToggle Boolean settings title, subtitle, isOn, accentColor
SettingsSlider Numeric settings value, range, format, accentColor
SettingsNavigationRow Navigate to detail title, subtitle, backgroundColor
SettingsSegmentedPicker Option selection title, subtitle, options, accentColor
SegmentedPicker Low-level picker title, options, selection
SettingsRow Action rows systemImage, title, iconColor
SelectableRow Card selection title, isSelected, badge
BadgePill Price/tag badges text, isSelected
iCloudSyncSettingsView iCloud sync controls viewModel: CloudSyncable, colors
LicensesView Open source licenses licenses: [License], colors

Light & Dark Mode Support

All settings components use SwiftUI's semantic colors (.primary, .secondary, .tertiary) which automatically adapt to the system appearance. No extra configuration needed—components work in both light and dark mode out of the box.

To force a specific appearance for your settings screen:

SettingsView()
    .preferredColorScheme(.dark)  // Force dark mode

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 iOS semantic fonts (.body, .caption, etc.) that scale properly with Dynamic Type.

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

  7. Use titleAccessory for badges: Instead of creating custom toggle views, use the titleAccessory parameter to add crown icons, badges, etc.

  8. Prefer Bedrock components: Before writing custom UI, check if a Bedrock component exists. This ensures consistency and reduces code duplication.


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.