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
|
```swift
|
||||||
Text("Primary Text")
|
Text("Primary Text")
|
||||||
.foregroundStyle(Color.Text.primary)
|
.foregroundStyle(Color.TextColors.primary)
|
||||||
|
|
||||||
Text("Secondary Text")
|
Text("Secondary Text")
|
||||||
.foregroundStyle(Color.Text.secondary)
|
.foregroundStyle(Color.TextColors.secondary)
|
||||||
|
|
||||||
VStack { }
|
VStack { }
|
||||||
.background(Color.Surface.primary)
|
.background(Color.Surface.primary)
|
||||||
|
|||||||
@ -113,18 +113,23 @@ public enum DefaultTheme: AppColorTheme {
|
|||||||
/// These provide the familiar `Color.Surface.primary` syntax using the
|
/// These provide the familiar `Color.Surface.primary` syntax using the
|
||||||
/// default theme. Apps using custom themes should access colors through
|
/// default theme. Apps using custom themes should access colors through
|
||||||
/// their theme type directly (e.g., `CasinoTheme.Surface.primary`).
|
/// 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 {
|
public extension Color {
|
||||||
/// Default surface colors.
|
/// Default surface colors.
|
||||||
typealias Surface = DefaultSurfaceColors
|
typealias Surface = DefaultSurfaceColors
|
||||||
|
|
||||||
/// Default text colors.
|
/// Default text colors.
|
||||||
typealias Text = DefaultTextColors
|
/// Note: Named `TextColors` to avoid conflict with SwiftUI's `Text` view.
|
||||||
|
typealias TextColors = DefaultTextColors
|
||||||
|
|
||||||
/// Default accent colors.
|
/// Default accent colors.
|
||||||
typealias Accent = DefaultAccentColors
|
typealias Accent = DefaultAccentColors
|
||||||
|
|
||||||
/// Default button colors.
|
/// Default button colors.
|
||||||
typealias Button = DefaultButtonColors
|
/// Note: Named `ButtonColors` to avoid conflict with SwiftUI's `Button` view.
|
||||||
|
typealias ButtonColors = DefaultButtonColors
|
||||||
|
|
||||||
/// Default status colors.
|
/// Default status colors.
|
||||||
typealias Status = DefaultStatusColors
|
typealias Status = DefaultStatusColors
|
||||||
|
|||||||
@ -74,7 +74,7 @@ public extension View {
|
|||||||
.pulsing(isActive: true)
|
.pulsing(isActive: true)
|
||||||
|
|
||||||
Text("Pulsing highlights interactive areas")
|
Text("Pulsing highlights interactive areas")
|
||||||
.foregroundStyle(Color.Text.secondary)
|
.foregroundStyle(Color.TextColors.secondary)
|
||||||
.font(.caption)
|
.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.
|
/// Optional system image name.
|
||||||
public let systemImage: String?
|
public let systemImage: String?
|
||||||
|
|
||||||
|
/// The accent color for the header.
|
||||||
|
public let accentColor: Color
|
||||||
|
|
||||||
/// Creates a section header.
|
/// Creates a section header.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - title: The section title.
|
/// - title: The section title.
|
||||||
/// - systemImage: Optional SF Symbol name.
|
/// - 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.title = title
|
||||||
self.systemImage = systemImage
|
self.systemImage = systemImage
|
||||||
|
self.accentColor = accentColor
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
@ -426,7 +431,7 @@ public struct SettingsSectionHeader: View {
|
|||||||
if let systemImage {
|
if let systemImage {
|
||||||
Image(systemName: systemImage)
|
Image(systemName: systemImage)
|
||||||
.font(.system(size: Design.BaseFontSize.medium))
|
.font(.system(size: Design.BaseFontSize.medium))
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
.foregroundStyle(accentColor.opacity(Design.Opacity.strong))
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(title)
|
Text(title)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user