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
This commit is contained in:
parent
c7c507c8f7
commit
cc650c68d4
@ -67,10 +67,10 @@ Bedrock includes neutral default colors that work out of the box:
|
||||
|
||||
```swift
|
||||
Text("Primary Text")
|
||||
.foregroundStyle(Color.Text.primary)
|
||||
.foregroundStyle(Color.TextColors.primary)
|
||||
|
||||
Text("Secondary Text")
|
||||
.foregroundStyle(Color.Text.secondary)
|
||||
.foregroundStyle(Color.TextColors.secondary)
|
||||
|
||||
VStack { }
|
||||
.background(Color.Surface.primary)
|
||||
|
||||
@ -113,18 +113,23 @@ public enum DefaultTheme: AppColorTheme {
|
||||
/// These provide the familiar `Color.Surface.primary` syntax using the
|
||||
/// default theme. Apps using custom themes should access colors through
|
||||
/// their theme type directly (e.g., `CasinoTheme.Surface.primary`).
|
||||
///
|
||||
/// Note: `TextColors` and `ButtonColors` are used instead of `Text` and `Button`
|
||||
/// to avoid conflicts with SwiftUI's `Text` and `Button` views.
|
||||
public extension Color {
|
||||
/// Default surface colors.
|
||||
typealias Surface = DefaultSurfaceColors
|
||||
|
||||
/// Default text colors.
|
||||
typealias Text = DefaultTextColors
|
||||
/// Note: Named `TextColors` to avoid conflict with SwiftUI's `Text` view.
|
||||
typealias TextColors = DefaultTextColors
|
||||
|
||||
/// Default accent colors.
|
||||
typealias Accent = DefaultAccentColors
|
||||
|
||||
/// Default button colors.
|
||||
typealias Button = DefaultButtonColors
|
||||
/// Note: Named `ButtonColors` to avoid conflict with SwiftUI's `Button` view.
|
||||
typealias ButtonColors = DefaultButtonColors
|
||||
|
||||
/// Default status colors.
|
||||
typealias Status = DefaultStatusColors
|
||||
|
||||
@ -74,7 +74,7 @@ public extension View {
|
||||
.pulsing(isActive: true)
|
||||
|
||||
Text("Pulsing highlights interactive areas")
|
||||
.foregroundStyle(Color.Text.secondary)
|
||||
.foregroundStyle(Color.TextColors.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
391
Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md
Normal file
391
Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md
Normal file
@ -0,0 +1,391 @@
|
||||
# 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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
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
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
/// 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:
|
||||
|
||||
```swift
|
||||
/// 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:
|
||||
|
||||
```swift
|
||||
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.
|
||||
|
||||
```swift
|
||||
SettingsSectionHeader(
|
||||
title: "Account",
|
||||
systemImage: "person.circle",
|
||||
accentColor: Color.Accent.primary
|
||||
)
|
||||
```
|
||||
|
||||
### SettingsToggle
|
||||
A toggle row with title and subtitle.
|
||||
|
||||
```swift
|
||||
SettingsToggle(
|
||||
title: "Sound Effects",
|
||||
subtitle: "Play sounds for events",
|
||||
isOn: $viewModel.soundEnabled,
|
||||
accentColor: Color.Accent.primary
|
||||
)
|
||||
```
|
||||
|
||||
### SegmentedPicker
|
||||
A horizontal capsule-style picker.
|
||||
|
||||
```swift
|
||||
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.
|
||||
|
||||
```swift
|
||||
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.
|
||||
|
||||
```swift
|
||||
VolumePicker(
|
||||
label: "Volume",
|
||||
volume: $viewModel.volume,
|
||||
accentColor: Color.Accent.primary
|
||||
)
|
||||
```
|
||||
|
||||
### SettingsRow
|
||||
A navigation-style row with icon and chevron.
|
||||
|
||||
```swift
|
||||
SettingsRow(
|
||||
systemImage: "star.fill",
|
||||
title: "Rate App",
|
||||
iconColor: Color.Status.warning
|
||||
) {
|
||||
openAppStore()
|
||||
}
|
||||
```
|
||||
|
||||
### BadgePill
|
||||
A capsule badge for values or tags.
|
||||
|
||||
```swift
|
||||
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.
|
||||
@ -412,13 +412,18 @@ public struct SettingsSectionHeader: View {
|
||||
/// Optional system image name.
|
||||
public let systemImage: String?
|
||||
|
||||
/// The accent color for the header.
|
||||
public let accentColor: Color
|
||||
|
||||
/// Creates a section header.
|
||||
/// - Parameters:
|
||||
/// - title: The section title.
|
||||
/// - systemImage: Optional SF Symbol name.
|
||||
public init(title: String, systemImage: String? = nil) {
|
||||
/// - accentColor: The accent color (default: primary accent).
|
||||
public init(title: String, systemImage: String? = nil, accentColor: Color = .Accent.primary) {
|
||||
self.title = title
|
||||
self.systemImage = systemImage
|
||||
self.accentColor = accentColor
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
@ -426,7 +431,7 @@ public struct SettingsSectionHeader: View {
|
||||
if let systemImage {
|
||||
Image(systemName: systemImage)
|
||||
.font(.system(size: Design.BaseFontSize.medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
.foregroundStyle(accentColor.opacity(Design.Opacity.strong))
|
||||
}
|
||||
|
||||
Text(title)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user