Refactor SettingsView to use Bedrock components: SettingsCard, SettingsNavigationRow, SettingsSlider, and titleAccessory

This commit is contained in:
Matt Bruce 2026-01-04 17:50:28 -06:00
parent 003d933521
commit 1b92467ec3
3 changed files with 68 additions and 192 deletions

View File

@ -30,7 +30,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: "Ring Light", systemImage: "light.max", accentColor: AppAccent.primary) SettingsSectionHeader(title: "Ring Light", systemImage: "light.max", accentColor: AppAccent.primary)
SettingsCard { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// Ring Light Enabled // Ring Light Enabled
SettingsToggle( SettingsToggle(
title: String(localized: "Enable Ring Light"), title: String(localized: "Enable Ring Light"),
@ -54,7 +54,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: "Camera Controls", systemImage: "camera", accentColor: AppAccent.primary) SettingsSectionHeader(title: "Camera Controls", systemImage: "camera", accentColor: AppAccent.primary)
SettingsCard { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// Camera Position // Camera Position
cameraPositionPicker cameraPositionPicker
@ -83,7 +83,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: "Display", systemImage: "eye", accentColor: AppAccent.primary) SettingsSectionHeader(title: "Display", systemImage: "eye", accentColor: AppAccent.primary)
SettingsCard { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// True Mirror (premium) // True Mirror (premium)
premiumToggle( premiumToggle(
title: String(localized: "True Mirror"), title: String(localized: "True Mirror"),
@ -113,7 +113,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle", accentColor: AppAccent.primary) SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle", accentColor: AppAccent.primary)
SettingsCard { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// Timer Selection // Timer Selection
timerPicker timerPicker
@ -136,7 +136,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud", accentColor: AppAccent.primary) SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud", accentColor: AppAccent.primary)
SettingsCard { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
iCloudSyncSection iCloudSyncSection
} }
@ -151,7 +151,7 @@ struct SettingsView: View {
#if DEBUG #if DEBUG
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error) SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error)
SettingsCard { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
brandingDebugSection brandingDebugSection
} }
#endif #endif
@ -175,57 +175,35 @@ struct SettingsView: View {
} }
// MARK: - Ring Size Slider // MARK: - Ring Size Slider
private var ringSizeSlider: some View { private var ringSizeSlider: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { SettingsSlider(
HStack { title: String(localized: "Ring Size"),
Text(String(localized: "Ring Size")) subtitle: String(localized: "Adjusts the size of the light ring around the camera preview"),
.font(.system(size: Design.BaseFontSize.medium, weight: .medium)) value: $viewModel.ringSize,
.foregroundStyle(.white) in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize,
step: 5,
Spacer() format: SliderFormat.integer(unit: "pt"),
accentColor: AppAccent.primary,
Text("\(Int(viewModel.ringSize))pt") leadingIcon: Image(systemName: "circle"),
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) trailingIcon: Image(systemName: "circle")
.foregroundStyle(.white.opacity(Design.Opacity.medium)) )
}
HStack(spacing: Design.Spacing.medium) {
// Small ring icon
Image(systemName: "circle")
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Slider(
value: $viewModel.ringSize,
in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize,
step: 5
)
.tint(AppAccent.primary)
// Large ring icon
Image(systemName: "circle")
.font(.system(size: Design.BaseFontSize.large))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Text(String(localized: "Adjusts the size of the light ring around the camera preview"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
.padding(.vertical, Design.Spacing.xSmall)
.accessibilityLabel(String(localized: "Ring size")) .accessibilityLabel(String(localized: "Ring size"))
.accessibilityValue("\(Int(viewModel.ringSize)) points") .accessibilityValue("\(Int(viewModel.ringSize)) points")
} }
// MARK: - Color Preset Section // MARK: - Color Preset Section
private var colorPresetSection: some View { private var colorPresetSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Light Color")) Text(String(localized: "Light Color"))
.font(.system(size: Design.BaseFontSize.medium, weight: .medium)) .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white) .foregroundStyle(.white)
Text(String(localized: "Choose the color of the ring light around the camera preview"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
LazyVGrid( LazyVGrid(
columns: [GridItem(.adaptive(minimum: 80), spacing: Design.Spacing.small)], columns: [GridItem(.adaptive(minimum: 80), spacing: Design.Spacing.small)],
spacing: Design.Spacing.small spacing: Design.Spacing.small
@ -248,7 +226,7 @@ struct SettingsView: View {
} }
} }
} }
// Custom color picker (premium) - one-step: opens picker, selects on change // Custom color picker (premium) - one-step: opens picker, selects on change
CustomColorPickerButton( CustomColorPickerButton(
customColor: Binding( customColor: Binding(
@ -410,41 +388,17 @@ struct SettingsView: View {
// MARK: - Ring Light Brightness Slider // MARK: - Ring Light Brightness Slider
private var ringLightBrightnessSlider: some View { private var ringLightBrightnessSlider: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { SettingsSlider(
HStack { title: String(localized: "Ring Light Brightness"),
Text(String(localized: "Ring Light Brightness")) subtitle: String(localized: "Adjusts the brightness of the ring light"),
.font(.system(size: Design.BaseFontSize.medium, weight: .medium)) value: $viewModel.ringLightOpacity,
.foregroundStyle(.white) in: 0.1...1.0,
step: 0.05,
Spacer() format: SliderFormat.percentage,
accentColor: AppAccent.primary,
Text("\(Int(viewModel.ringLightOpacity * 100))%") leadingIcon: Image(systemName: "sun.min"),
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) trailingIcon: Image(systemName: "sun.max.fill")
.foregroundStyle(.white.opacity(Design.Opacity.medium)) )
}
HStack(spacing: Design.Spacing.medium) {
Image(systemName: "sun.min")
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Slider(
value: $viewModel.ringLightOpacity,
in: 0.1...1.0,
step: 0.05
)
.tint(AppAccent.primary)
Image(systemName: "sun.max.fill")
.font(.system(size: Design.BaseFontSize.large))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Text(String(localized: "Adjusts the brightness of the ring light"))
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
.padding(.vertical, Design.Spacing.xSmall)
.accessibilityLabel(String(localized: "Ring light brightness")) .accessibilityLabel(String(localized: "Ring light brightness"))
.accessibilityValue("\(Int(viewModel.ringLightOpacity * 100)) percent") .accessibilityValue("\(Int(viewModel.ringLightOpacity * 100)) percent")
} }
@ -668,31 +622,12 @@ struct SettingsView: View {
// MARK: - Acknowledgments Section // MARK: - Acknowledgments Section
private var acknowledgmentsSection: some View { private var acknowledgmentsSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { SettingsNavigationRow(
NavigationLink { title: String(localized: "Open Source Licenses"),
LicensesView() subtitle: String(localized: "Third-party libraries used in this app"),
} label: { backgroundColor: AppSurface.primary
HStack { ) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { LicensesView()
Text(String(localized: "Open Source Licenses"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
Text(String(localized: "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)
} }
} }
@ -712,25 +647,16 @@ struct SettingsView: View {
isOn: Binding<Bool>, isOn: Binding<Bool>,
accessibilityHint: String accessibilityHint: String
) -> some View { ) -> some View {
Toggle(isOn: isOn) { SettingsToggle(
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { title: title,
HStack(spacing: Design.Spacing.xSmall) { subtitle: subtitle,
Text(title) isOn: isOn,
.font(.system(size: Design.BaseFontSize.medium, weight: .medium)) accentColor: AppAccent.primary,
.foregroundStyle(.white) titleAccessory: {
Image(systemName: "crown.fill")
Image(systemName: "crown.fill") .foregroundStyle(AppStatus.warning)
.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)
.padding(.vertical, Design.Spacing.xSmall)
.disabled(!isPremiumUnlocked) .disabled(!isPremiumUnlocked)
.accessibilityHint(accessibilityHint) .accessibilityHint(accessibilityHint)
.onTapGesture { .onTapGesture {
@ -783,61 +709,28 @@ struct SettingsView: View {
isOn: $viewModel.isDebugPremiumEnabled, isOn: $viewModel.isDebugPremiumEnabled,
accentColor: AppStatus.warning accentColor: AppStatus.warning
) )
// Icon Generator // Icon Generator
NavigationLink { SettingsNavigationRow(
title: "Icon Generator",
subtitle: "Generate and save app icon to Files",
backgroundColor: AppSurface.primary
) {
IconGeneratorView(config: .selfieCam, appName: "SelfieCam") IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Icon Generator")
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
Text("Generate and save app icon to Files")
.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)
// Branding Preview // Branding Preview
NavigationLink { SettingsNavigationRow(
title: "Branding Preview",
subtitle: "Preview app icon and launch screen",
backgroundColor: AppSurface.primary
) {
BrandingPreviewView( BrandingPreviewView(
iconConfig: .selfieCam, iconConfig: .selfieCam,
launchConfig: .selfieCam, launchConfig: .selfieCam,
appName: "SelfieCam" appName: "SelfieCam"
) )
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Branding Preview")
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
Text("Preview app icon and launch screen")
.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)
} }
} }
#endif #endif
@ -845,28 +738,6 @@ struct SettingsView: View {
// MARK: - Settings Card Container
/// A card container that provides visual grouping for settings sections.
/// Uses the app's branded surface colors for separation from the background.
private 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)
)
}
}
#Preview { #Preview {
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false)) SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))

View File

@ -43,7 +43,7 @@ final class SettingsViewModel: RingLightConfigurable {
// MARK: - Ring Size Limits // MARK: - Ring Size Limits
/// Minimum ring border size in points /// Minimum ring border size in points
static let minRingSize: CGFloat = 10 static let minRingSize: CGFloat = 40
/// Maximum ring border size in points /// Maximum ring border size in points
static let maxRingSize: CGFloat = 120 static let maxRingSize: CGFloat = 120

View File

@ -123,6 +123,7 @@
}, },
"%lldpt" : { "%lldpt" : {
"comment" : "A label displaying the current ring size, formatted as a number followed by the unit \"pt\".", "comment" : "A label displaying the current ring size, formatted as a number followed by the unit \"pt\".",
"extractionState" : "stale",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
"localizations" : { "localizations" : {
"es-MX" : { "es-MX" : {
@ -770,6 +771,10 @@
} }
} }
}, },
"Choose the color of the ring light around the camera preview" : {
"comment" : "A description under the title of the light color preset section, explaining its purpose.",
"isCommentAutoGenerated" : true
},
"Close" : { "Close" : {
"comment" : "A button label that closes the view.", "comment" : "A button label that closes the view.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true