Add settings components: SettingsCard, SettingsNavigationRow, SettingsSlider, titleAccessory for SettingsToggle, and update SETTINGS_GUIDE with debug section pattern
This commit is contained in:
parent
cf46b9f1f4
commit
95a20ac5a3
@ -154,35 +154,7 @@ typealias AppInteractive = MyAppInteractiveColors
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 2: Create a Settings Card Container
|
## Step 2: Build Your Settings View
|
||||||
|
|
||||||
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:
|
Use Bedrock's components with your theme colors:
|
||||||
|
|
||||||
@ -206,7 +178,7 @@ struct SettingsView: View {
|
|||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Dark Mode",
|
title: "Dark Mode",
|
||||||
subtitle: "Use dark appearance",
|
subtitle: "Use dark appearance",
|
||||||
@ -229,7 +201,7 @@ struct SettingsView: View {
|
|||||||
accentColor: AppAccent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Push Notifications",
|
title: "Push Notifications",
|
||||||
subtitle: "Receive alerts and updates",
|
subtitle: "Receive alerts and updates",
|
||||||
@ -260,30 +232,265 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
## Available Components
|
## Available Components
|
||||||
|
|
||||||
|
### SettingsCard
|
||||||
|
|
||||||
|
A card container for visually grouping related settings.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||||
|
SettingsToggle(...)
|
||||||
|
SettingsSlider(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refactoring**: Replace inline card styling with `SettingsCard`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ 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
|
### SettingsSectionHeader
|
||||||
|
|
||||||
A section header with optional icon and accent color.
|
A section header with optional icon and accent color.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
SettingsSectionHeader(
|
SettingsSectionHeader(
|
||||||
title: "Account",
|
title: "Account",
|
||||||
systemImage: "person.circle",
|
systemImage: "person.circle",
|
||||||
accentColor: Color.Accent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### SettingsToggle
|
### SettingsToggle
|
||||||
A toggle row with title and subtitle.
|
|
||||||
|
A toggle row with title, subtitle, and optional title accessory (e.g., premium crown).
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
// Basic toggle
|
||||||
SettingsToggle(
|
SettingsToggle(
|
||||||
title: "Sound Effects",
|
title: "Sound Effects",
|
||||||
subtitle: "Play sounds for events",
|
subtitle: "Play sounds for events",
|
||||||
isOn: $viewModel.soundEnabled,
|
isOn: $viewModel.soundEnabled,
|
||||||
accentColor: Color.Accent.primary
|
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`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BEFORE: Custom premium toggle
|
||||||
|
Toggle(isOn: $isOn) {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
|
HStack(spacing: Design.Spacing.xSmall) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
Image(systemName: "crown.fill")
|
||||||
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
|
.foregroundStyle(AppStatus.warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: Design.BaseFontSize.body))
|
||||||
|
.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.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BEFORE: Inline slider with manual layout
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
|
HStack {
|
||||||
|
Text("Ring Size")
|
||||||
|
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(viewModel.ringSize))pt")
|
||||||
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Adjusts the size of the ring")
|
||||||
|
.font(.system(size: Design.BaseFontSize.caption))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
Image(systemName: "circle")
|
||||||
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
|
Slider(value: $viewModel.ringSize, in: 20...100, step: 5)
|
||||||
|
.tint(AppAccent.primary)
|
||||||
|
|
||||||
|
Image(systemName: "circle")
|
||||||
|
.font(.system(size: Design.BaseFontSize.large))
|
||||||
|
.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.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
SettingsNavigationRow(
|
||||||
|
title: "Open Source Licenses",
|
||||||
|
subtitle: "Third-party libraries used in this app",
|
||||||
|
backgroundColor: AppSurface.primary
|
||||||
|
) {
|
||||||
|
LicensesView()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refactoring**: Replace inline NavigationLink styling:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ BEFORE: Inline NavigationLink with custom styling
|
||||||
|
NavigationLink {
|
||||||
|
LicensesView()
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
|
Text("Open Source Licenses")
|
||||||
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
Text("Third-party libraries used in this app")
|
||||||
|
.font(.system(size: Design.BaseFontSize.caption))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.system(size: Design.BaseFontSize.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()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### SegmentedPicker
|
### SegmentedPicker
|
||||||
|
|
||||||
A horizontal capsule-style picker.
|
A horizontal capsule-style picker.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@ -291,11 +498,30 @@ SegmentedPicker(
|
|||||||
title: "Quality",
|
title: "Quality",
|
||||||
options: [("Low", 0), ("Medium", 1), ("High", 2)],
|
options: [("Low", 0), ("Medium", 1), ("High", 2)],
|
||||||
selection: $viewModel.quality,
|
selection: $viewModel.quality,
|
||||||
accentColor: Color.Accent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SettingsRow
|
||||||
|
|
||||||
|
An action row with icon and chevron (for non-navigation actions).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
SettingsRow(
|
||||||
|
systemImage: "star.fill",
|
||||||
|
title: "Rate App",
|
||||||
|
iconColor: AppStatus.warning
|
||||||
|
) {
|
||||||
|
openAppStore()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### SelectableRow
|
### SelectableRow
|
||||||
|
|
||||||
A card-like row for option selection.
|
A card-like row for option selection.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
@ -303,50 +529,207 @@ SelectableRow(
|
|||||||
title: "Premium",
|
title: "Premium",
|
||||||
subtitle: "Unlock all features",
|
subtitle: "Unlock all features",
|
||||||
isSelected: plan == .premium,
|
isSelected: plan == .premium,
|
||||||
accentColor: Color.Accent.primary,
|
accentColor: AppAccent.primary,
|
||||||
badge: { BadgePill(text: "$9.99") }
|
badge: { BadgePill(text: "$9.99") }
|
||||||
) {
|
) {
|
||||||
plan = .premium
|
plan = .premium
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### VolumePicker
|
### VolumePicker
|
||||||
|
|
||||||
A slider with speaker icons for volume/percentage values.
|
A slider with speaker icons for volume/percentage values.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
VolumePicker(
|
VolumePicker(
|
||||||
label: "Volume",
|
label: "Volume",
|
||||||
volume: $viewModel.volume,
|
volume: $viewModel.volume,
|
||||||
accentColor: Color.Accent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### SettingsRow
|
---
|
||||||
A navigation-style row with icon and chevron.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
SettingsRow(
|
|
||||||
systemImage: "star.fill",
|
|
||||||
title: "Rate App",
|
|
||||||
iconColor: Color.Status.warning
|
|
||||||
) {
|
|
||||||
openAppStore()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### BadgePill
|
### BadgePill
|
||||||
|
|
||||||
A capsule badge for values or tags.
|
A capsule badge for values or tags.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
BadgePill(
|
BadgePill(
|
||||||
text: "$4.99",
|
text: "$4.99",
|
||||||
isSelected: isCurrentPlan,
|
isSelected: isCurrentPlan,
|
||||||
accentColor: Color.Accent.primary
|
accentColor: AppAccent.primary
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Debug Section Pattern
|
||||||
|
|
||||||
|
Add a debug section to settings for developer tools. This section is only visible in DEBUG builds.
|
||||||
|
|
||||||
|
### Standard Debug Section Structure
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
#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:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
#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
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// ❌ 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`, `titleAccessory` |
|
||||||
|
| `SettingsSlider` | Numeric settings | `value`, `range`, `format`, icons |
|
||||||
|
| `SettingsNavigationRow` | Navigate to detail | `title`, `subtitle`, `destination` |
|
||||||
|
| `SegmentedPicker` | Option selection | `title`, `options`, `selection` |
|
||||||
|
| `SettingsRow` | Action rows | `systemImage`, `title`, `action` |
|
||||||
|
| `SelectableRow` | Card selection | `title`, `isSelected`, `badge` |
|
||||||
|
| `VolumePicker` | Audio volume | `label`, `volume` |
|
||||||
|
| `BadgePill` | Price/tag badges | `text`, `isSelected` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Color Relationship Guide
|
## Color Relationship Guide
|
||||||
|
|
||||||
| Surface Level | Use Case | Visual Depth |
|
| Surface Level | Use Case | Visual Depth |
|
||||||
@ -380,6 +763,10 @@ BadgePill(
|
|||||||
|
|
||||||
6. **Avoid `Color.` typealiases**: Use `App`-prefixed typealiases to prevent conflicts with Bedrock's defaults.
|
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
|
## Example Apps
|
||||||
|
|||||||
86
Sources/Bedrock/Views/Settings/SettingsCard.swift
Normal file
86
Sources/Bedrock/Views/Settings/SettingsCard.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// SettingsCard.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// A card container for grouping settings sections.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A card container that provides visual grouping for settings sections.
|
||||||
|
///
|
||||||
|
/// Use this to visually separate groups of related settings controls.
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// SettingsCard {
|
||||||
|
/// SettingsToggle(title: "Dark Mode", ...)
|
||||||
|
/// SettingsToggle(title: "Notifications", ...)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
public struct SettingsCard<Content: View>: View {
|
||||||
|
/// The content views inside the card.
|
||||||
|
@ViewBuilder public let content: Content
|
||||||
|
|
||||||
|
/// The card background color.
|
||||||
|
public let backgroundColor: Color
|
||||||
|
|
||||||
|
/// The card border color.
|
||||||
|
public let borderColor: Color
|
||||||
|
|
||||||
|
/// Creates a settings card.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - backgroundColor: The card background (default: Surface.card).
|
||||||
|
/// - borderColor: The card border (default: Border.subtle).
|
||||||
|
/// - content: The content views.
|
||||||
|
public init(
|
||||||
|
backgroundColor: Color = .Surface.card,
|
||||||
|
borderColor: Color = .Border.subtle,
|
||||||
|
@ViewBuilder content: () -> Content
|
||||||
|
) {
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.borderColor = borderColor
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.padding(Design.Spacing.medium)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||||
|
.fill(backgroundColor)
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||||
|
.strokeBorder(borderColor, lineWidth: Design.LineWidth.thin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack(spacing: Design.Spacing.medium) {
|
||||||
|
SettingsCard {
|
||||||
|
SettingsToggle(
|
||||||
|
title: "Dark Mode",
|
||||||
|
subtitle: "Use dark appearance",
|
||||||
|
isOn: .constant(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsToggle(
|
||||||
|
title: "Notifications",
|
||||||
|
subtitle: "Receive push notifications",
|
||||||
|
isOn: .constant(false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard(backgroundColor: .Surface.overlay) {
|
||||||
|
Text("Custom background")
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.Surface.overlay)
|
||||||
|
}
|
||||||
112
Sources/Bedrock/Views/Settings/SettingsNavigationRow.swift
Normal file
112
Sources/Bedrock/Views/Settings/SettingsNavigationRow.swift
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// SettingsNavigationRow.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// A navigation link row for settings screens.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A navigation link row for settings screens.
|
||||||
|
///
|
||||||
|
/// Use this for settings that navigate to a detail view.
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// SettingsNavigationRow(
|
||||||
|
/// title: "Open Source Licenses",
|
||||||
|
/// subtitle: "Third-party libraries used in this app"
|
||||||
|
/// ) {
|
||||||
|
/// LicensesView()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
public struct SettingsNavigationRow<Destination: View>: View {
|
||||||
|
/// The row title.
|
||||||
|
public let title: String
|
||||||
|
|
||||||
|
/// Optional subtitle text.
|
||||||
|
public let subtitle: String?
|
||||||
|
|
||||||
|
/// The background color.
|
||||||
|
public let backgroundColor: Color
|
||||||
|
|
||||||
|
/// The destination view.
|
||||||
|
public let destination: Destination
|
||||||
|
|
||||||
|
/// Creates a settings navigation row.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - title: The row title.
|
||||||
|
/// - subtitle: Optional subtitle text.
|
||||||
|
/// - backgroundColor: The background color (default: Surface.primary).
|
||||||
|
/// - destination: The destination view.
|
||||||
|
public init(
|
||||||
|
title: String,
|
||||||
|
subtitle: String? = nil,
|
||||||
|
backgroundColor: Color = .Surface.primary,
|
||||||
|
@ViewBuilder destination: () -> Destination
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.destination = destination()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
NavigationLink {
|
||||||
|
destination
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
if let subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: Design.BaseFontSize.caption))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.system(size: Design.BaseFontSize.caption))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
.padding(Design.Spacing.medium)
|
||||||
|
.background(backgroundColor, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
VStack(spacing: Design.Spacing.small) {
|
||||||
|
SettingsNavigationRow(
|
||||||
|
title: "Open Source Licenses",
|
||||||
|
subtitle: "Third-party libraries used in this app"
|
||||||
|
) {
|
||||||
|
Text("Licenses Detail View")
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsNavigationRow(
|
||||||
|
title: "Privacy Policy"
|
||||||
|
) {
|
||||||
|
Text("Privacy Policy View")
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsNavigationRow(
|
||||||
|
title: "Custom Background",
|
||||||
|
subtitle: "With overlay color",
|
||||||
|
backgroundColor: .Surface.overlay
|
||||||
|
) {
|
||||||
|
Text("Detail View")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.Surface.overlay)
|
||||||
|
}
|
||||||
|
}
|
||||||
206
Sources/Bedrock/Views/Settings/SettingsSlider.swift
Normal file
206
Sources/Bedrock/Views/Settings/SettingsSlider.swift
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
//
|
||||||
|
// SettingsSlider.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// A slider setting with title, description, and value display.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A slider setting with title, description, and value display.
|
||||||
|
///
|
||||||
|
/// Use this for numeric settings with a slider control, following the pattern:
|
||||||
|
/// - Title with current value on the right
|
||||||
|
/// - Subtitle/description underneath the title
|
||||||
|
/// - Slider with optional icons underneath
|
||||||
|
///
|
||||||
|
/// ## Basic Usage
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// SettingsSlider(
|
||||||
|
/// title: "Ring Size",
|
||||||
|
/// subtitle: "Adjusts the size of the light ring",
|
||||||
|
/// value: $settings.ringSize,
|
||||||
|
/// in: 20...100,
|
||||||
|
/// step: 5,
|
||||||
|
/// format: { "\(Int($0))pt" }
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## With Icons
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// SettingsSlider(
|
||||||
|
/// title: "Brightness",
|
||||||
|
/// subtitle: "Adjusts the brightness",
|
||||||
|
/// value: $brightness,
|
||||||
|
/// format: .percentage,
|
||||||
|
/// leadingIcon: Image(systemName: "sun.min"),
|
||||||
|
/// trailingIcon: Image(systemName: "sun.max.fill")
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where Value.Stride: BinaryFloatingPoint {
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
/// The main title text.
|
||||||
|
public let title: String
|
||||||
|
|
||||||
|
/// The subtitle/description text.
|
||||||
|
public let subtitle: String
|
||||||
|
|
||||||
|
/// Binding to the slider value.
|
||||||
|
@Binding public var value: Value
|
||||||
|
|
||||||
|
/// The range of the slider.
|
||||||
|
public let range: ClosedRange<Value>
|
||||||
|
|
||||||
|
/// The step increment for the slider.
|
||||||
|
public let step: Value.Stride
|
||||||
|
|
||||||
|
/// A closure that formats the value for display.
|
||||||
|
public let format: (Value) -> String
|
||||||
|
|
||||||
|
/// The accent color for the slider.
|
||||||
|
public let accentColor: Color
|
||||||
|
|
||||||
|
/// Optional leading icon for the slider.
|
||||||
|
public let leadingIcon: Image?
|
||||||
|
|
||||||
|
/// Optional trailing icon for the slider.
|
||||||
|
public let trailingIcon: Image?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
/// Creates a settings slider.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - title: The main title.
|
||||||
|
/// - subtitle: The subtitle description.
|
||||||
|
/// - value: Binding to slider value.
|
||||||
|
/// - range: The range of values (default: 0...1).
|
||||||
|
/// - step: The step increment (default: 0.1).
|
||||||
|
/// - format: Closure to format the value display.
|
||||||
|
/// - accentColor: The accent color (default: primary accent).
|
||||||
|
/// - leadingIcon: Optional icon on the left side of slider.
|
||||||
|
/// - trailingIcon: Optional icon on the right side of slider.
|
||||||
|
public init(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
value: Binding<Value>,
|
||||||
|
in range: ClosedRange<Value> = 0...1,
|
||||||
|
step: Value.Stride = 0.1,
|
||||||
|
format: @escaping (Value) -> String,
|
||||||
|
accentColor: Color = .Accent.primary,
|
||||||
|
leadingIcon: Image? = nil,
|
||||||
|
trailingIcon: Image? = nil
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self._value = value
|
||||||
|
self.range = range
|
||||||
|
self.step = step
|
||||||
|
self.format = format
|
||||||
|
self.accentColor = accentColor
|
||||||
|
self.leadingIcon = leadingIcon
|
||||||
|
self.trailingIcon = trailingIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(format(value))
|
||||||
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: Design.BaseFontSize.caption))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
|
if let leadingIcon {
|
||||||
|
leadingIcon
|
||||||
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(value: $value, in: range, step: step)
|
||||||
|
.tint(accentColor)
|
||||||
|
|
||||||
|
if let trailingIcon {
|
||||||
|
trailingIcon
|
||||||
|
.font(.system(size: Design.BaseFontSize.large))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Format Helpers
|
||||||
|
|
||||||
|
/// Common format closures for SettingsSlider.
|
||||||
|
public enum SliderFormat {
|
||||||
|
|
||||||
|
/// Formats value as percentage (0.5 → "50%").
|
||||||
|
public static func percentage<V: BinaryFloatingPoint>(_ value: V) -> String {
|
||||||
|
"\(Int(Double(value) * 100))%"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats value as integer with unit suffix (40 → "40pt").
|
||||||
|
public static func integer<V: BinaryFloatingPoint>(unit: String) -> (V) -> String {
|
||||||
|
{ "\(Int($0))\(unit)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats value as decimal with specified precision.
|
||||||
|
public static func decimal<V: BinaryFloatingPoint>(precision: Int, unit: String = "") -> (V) -> String {
|
||||||
|
{ String(format: "%.\(precision)f\(unit)", Double($0)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack(spacing: Design.Spacing.large) {
|
||||||
|
SettingsSlider(
|
||||||
|
title: "Ring Size",
|
||||||
|
subtitle: "Adjusts the size of the light ring around the camera preview",
|
||||||
|
value: .constant(40.0),
|
||||||
|
in: 20...100,
|
||||||
|
step: 5,
|
||||||
|
format: SliderFormat.integer(unit: "pt"),
|
||||||
|
leadingIcon: Image(systemName: "circle"),
|
||||||
|
trailingIcon: Image(systemName: "circle")
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsSlider(
|
||||||
|
title: "Brightness",
|
||||||
|
subtitle: "Adjusts the brightness of the ring light",
|
||||||
|
value: .constant(0.75),
|
||||||
|
step: 0.05,
|
||||||
|
format: SliderFormat.percentage,
|
||||||
|
leadingIcon: Image(systemName: "sun.min"),
|
||||||
|
trailingIcon: Image(systemName: "sun.max.fill")
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsSlider(
|
||||||
|
title: "Zoom",
|
||||||
|
subtitle: "Camera zoom level",
|
||||||
|
value: .constant(1.5),
|
||||||
|
in: 1.0...5.0,
|
||||||
|
step: 0.1,
|
||||||
|
format: SliderFormat.decimal(precision: 1, unit: "x")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.Surface.overlay)
|
||||||
|
}
|
||||||
@ -12,13 +12,23 @@ import SwiftUI
|
|||||||
/// Use this for boolean settings that can be turned on or off.
|
/// Use this for boolean settings that can be turned on or off.
|
||||||
///
|
///
|
||||||
/// ```swift
|
/// ```swift
|
||||||
|
/// // Basic toggle
|
||||||
/// SettingsToggle(
|
/// SettingsToggle(
|
||||||
/// title: "Dark Mode",
|
/// title: "Dark Mode",
|
||||||
/// subtitle: "Use dark appearance",
|
/// subtitle: "Use dark appearance",
|
||||||
/// isOn: $settings.darkMode
|
/// isOn: $settings.darkMode
|
||||||
/// )
|
/// )
|
||||||
|
///
|
||||||
|
/// // Premium toggle with crown icon
|
||||||
|
/// SettingsToggle(
|
||||||
|
/// title: "Flash Sync",
|
||||||
|
/// subtitle: "Use ring light color for flash",
|
||||||
|
/// isOn: $settings.flashSync,
|
||||||
|
/// titleAccessory: Image(systemName: "crown.fill")
|
||||||
|
/// .foregroundStyle(.orange)
|
||||||
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
public struct SettingsToggle: View {
|
public struct SettingsToggle<Accessory: View>: View {
|
||||||
/// The main title text.
|
/// The main title text.
|
||||||
public let title: String
|
public let title: String
|
||||||
|
|
||||||
@ -31,12 +41,58 @@ public struct SettingsToggle: View {
|
|||||||
/// The accent color for the toggle.
|
/// The accent color for the toggle.
|
||||||
public let accentColor: Color
|
public let accentColor: Color
|
||||||
|
|
||||||
|
/// Optional accessory view shown after the title (e.g., crown icon for premium).
|
||||||
|
public let titleAccessory: Accessory?
|
||||||
|
|
||||||
/// Creates a settings toggle.
|
/// Creates a settings toggle.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - title: The main title.
|
/// - title: The main title.
|
||||||
/// - subtitle: The subtitle description.
|
/// - subtitle: The subtitle description.
|
||||||
/// - isOn: Binding to toggle state.
|
/// - isOn: Binding to toggle state.
|
||||||
/// - accentColor: The accent color (default: primary accent).
|
/// - accentColor: The accent color (default: primary accent).
|
||||||
|
/// - titleAccessory: Optional view after title (e.g., premium crown).
|
||||||
|
public init(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
isOn: Binding<Bool>,
|
||||||
|
accentColor: Color = .Accent.primary,
|
||||||
|
@ViewBuilder titleAccessory: () -> Accessory? = { nil as EmptyView? }
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self._isOn = isOn
|
||||||
|
self.accentColor = accentColor
|
||||||
|
self.titleAccessory = titleAccessory()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Toggle(isOn: $isOn) {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
|
HStack(spacing: Design.Spacing.xSmall) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
if let titleAccessory {
|
||||||
|
titleAccessory
|
||||||
|
.font(.system(size: Design.BaseFontSize.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: Design.BaseFontSize.body))
|
||||||
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tint(accentColor)
|
||||||
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Convenience Initializer
|
||||||
|
|
||||||
|
extension SettingsToggle where Accessory == EmptyView {
|
||||||
|
/// Creates a settings toggle without a title accessory.
|
||||||
public init(
|
public init(
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
@ -47,22 +103,7 @@ public struct SettingsToggle: View {
|
|||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
self._isOn = isOn
|
self._isOn = isOn
|
||||||
self.accentColor = accentColor
|
self.accentColor = accentColor
|
||||||
}
|
self.titleAccessory = nil
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
Toggle(isOn: $isOn) {
|
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
|
||||||
Text(title)
|
|
||||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.system(size: Design.BaseFontSize.body))
|
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tint(accentColor)
|
|
||||||
.padding(.vertical, Design.Spacing.xSmall)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +122,16 @@ public struct SettingsToggle: View {
|
|||||||
subtitle: "Receive push notifications",
|
subtitle: "Receive push notifications",
|
||||||
isOn: .constant(false)
|
isOn: .constant(false)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SettingsToggle(
|
||||||
|
title: "Flash Sync",
|
||||||
|
subtitle: "Use ring light color for screen flash",
|
||||||
|
isOn: .constant(true),
|
||||||
|
titleAccessory: {
|
||||||
|
Image(systemName: "crown.fill")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Color.Surface.overlay)
|
.background(Color.Surface.overlay)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user