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:
- Color protocols for consistent theming (
SurfaceColorProvider,AccentColorProvider, etc.) - Reusable settings components (
SettingsToggle,SettingsCard,SegmentedPicker, etc.) - 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-levelApp-prefixed typealiases instead. Ensure ALL typealiases mentioned in the theme are created, as components likeSettingsToggleorSettingsCardrely 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
-
Derive surface colors from your brand: Add a subtle RGB tint (e.g., if brand is pink, surfaces should have a rose undertone).
-
Use consistent accent colors: Pass
accentColor: AppAccent.primaryto all Bedrock components. -
Group related settings: Wrap related toggles/pickers in a
SettingsCardfor visual hierarchy. -
Section-specific accents: Use
AppStatus.warningfor premium sections,AppStatus.errorfor debug. -
Test with Dynamic Type: Bedrock uses iOS semantic fonts (
.body,.caption, etc.) that scale properly with Dynamic Type. -
Avoid
Color.typealiases: UseApp-prefixed typealiases to prevent conflicts with Bedrock's defaults. -
Use
titleAccessoryfor badges: Instead of creating custom toggle views, use thetitleAccessoryparameter to add crown icons, badges, etc. -
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.