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)
SettingsCard {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// Ring Light Enabled
SettingsToggle(
title: String(localized: "Enable Ring Light"),
@ -54,7 +54,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: "Camera Controls", systemImage: "camera", accentColor: AppAccent.primary)
SettingsCard {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// Camera Position
cameraPositionPicker
@ -83,7 +83,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: "Display", systemImage: "eye", accentColor: AppAccent.primary)
SettingsCard {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// True Mirror (premium)
premiumToggle(
title: String(localized: "True Mirror"),
@ -113,7 +113,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle", accentColor: AppAccent.primary)
SettingsCard {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// Timer Selection
timerPicker
@ -136,7 +136,7 @@ struct SettingsView: View {
SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud", accentColor: AppAccent.primary)
SettingsCard {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
iCloudSyncSection
}
@ -151,7 +151,7 @@ struct SettingsView: View {
#if DEBUG
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error)
SettingsCard {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
brandingDebugSection
}
#endif
@ -177,43 +177,17 @@ struct SettingsView: View {
// MARK: - Ring Size Slider
private var ringSizeSlider: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
Text(String(localized: "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))
}
HStack(spacing: Design.Spacing.medium) {
// Small ring icon
Image(systemName: "circle")
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Slider(
SettingsSlider(
title: String(localized: "Ring Size"),
subtitle: String(localized: "Adjusts the size of the light ring around the camera preview"),
value: $viewModel.ringSize,
in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize,
step: 5
step: 5,
format: SliderFormat.integer(unit: "pt"),
accentColor: AppAccent.primary,
leadingIcon: Image(systemName: "circle"),
trailingIcon: Image(systemName: "circle")
)
.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"))
.accessibilityValue("\(Int(viewModel.ringSize)) points")
}
@ -226,6 +200,10 @@ struct SettingsView: View {
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.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(
columns: [GridItem(.adaptive(minimum: 80), spacing: Design.Spacing.small)],
spacing: Design.Spacing.small
@ -410,41 +388,17 @@ struct SettingsView: View {
// MARK: - Ring Light Brightness Slider
private var ringLightBrightnessSlider: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
Text(String(localized: "Ring Light Brightness"))
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white)
Spacer()
Text("\(Int(viewModel.ringLightOpacity * 100))%")
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded))
.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(
SettingsSlider(
title: String(localized: "Ring Light Brightness"),
subtitle: String(localized: "Adjusts the brightness of the ring light"),
value: $viewModel.ringLightOpacity,
in: 0.1...1.0,
step: 0.05
step: 0.05,
format: SliderFormat.percentage,
accentColor: AppAccent.primary,
leadingIcon: Image(systemName: "sun.min"),
trailingIcon: Image(systemName: "sun.max.fill")
)
.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"))
.accessibilityValue("\(Int(viewModel.ringLightOpacity * 100)) percent")
}
@ -668,31 +622,12 @@ struct SettingsView: View {
// MARK: - Acknowledgments Section
private var acknowledgmentsSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
NavigationLink {
SettingsNavigationRow(
title: String(localized: "Open Source Licenses"),
subtitle: String(localized: "Third-party libraries used in this app"),
backgroundColor: AppSurface.primary
) {
LicensesView()
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
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>,
accessibilityHint: String
) -> 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)
SettingsToggle(
title: title,
subtitle: subtitle,
isOn: isOn,
accentColor: AppAccent.primary,
titleAccessory: {
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)
.padding(.vertical, Design.Spacing.xSmall)
)
.disabled(!isPremiumUnlocked)
.accessibilityHint(accessibilityHint)
.onTapGesture {
@ -783,61 +709,28 @@ struct SettingsView: View {
isOn: $viewModel.isDebugPremiumEnabled,
accentColor: AppStatus.warning
)
// Icon Generator
NavigationLink {
SettingsNavigationRow(
title: "Icon Generator",
subtitle: "Generate and save app icon to Files",
backgroundColor: AppSurface.primary
) {
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
NavigationLink {
SettingsNavigationRow(
title: "Branding Preview",
subtitle: "Preview app icon and launch screen",
backgroundColor: AppSurface.primary
) {
BrandingPreviewView(
iconConfig: .selfieCam,
launchConfig: .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
@ -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 {
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))

View File

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

View File

@ -123,6 +123,7 @@
},
"%lldpt" : {
"comment" : "A label displaying the current ring size, formatted as a number followed by the unit \"pt\".",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"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" : {
"comment" : "A button label that closes the view.",
"isCommentAutoGenerated" : true