From 3e1d63740fa1bd1b6588b3b3c047605a84b59d16 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sun, 4 Jan 2026 18:03:50 -0600 Subject: [PATCH] Add SettingsSegmentedPicker with title, subtitle, and titleAccessory support --- .../Bedrock/Views/Settings/SETTINGS_GUIDE.md | 67 ++++++- .../Settings/SettingsSegmentedPicker.swift | 173 ++++++++++++++++++ 2 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift diff --git a/Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md b/Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md index c903be6..a9ab462 100644 --- a/Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md +++ b/Sources/Bedrock/Views/Settings/SETTINGS_GUIDE.md @@ -489,9 +489,69 @@ SettingsNavigationRow( --- -### SegmentedPicker +### SettingsSegmentedPicker -A horizontal capsule-style picker. +A segmented picker with title, subtitle, and optional accessory (follows the same pattern as `SettingsToggle` and `SettingsSlider`). + +```swift +// Basic picker +SettingsSegmentedPicker( + title: "Camera", + subtitle: "Choose between front and back camera lenses", + options: [("Front", .front), ("Back", .back)], + selection: $viewModel.cameraPosition, + accentColor: AppAccent.primary +) + +// Premium picker with crown icon +SettingsSegmentedPicker( + title: "HDR Mode", + subtitle: "High Dynamic Range for better lighting in photos", + options: [("Off", .off), ("On", .on), ("Auto", .auto)], + selection: $viewModel.hdrMode, + accentColor: AppAccent.primary, + titleAccessory: { + Image(systemName: "crown.fill") + .foregroundStyle(AppStatus.warning) + } +) +.disabled(!isPremiumUnlocked) +``` + +**Refactoring**: Replace inline segmented pickers: + +```swift +// ❌ BEFORE: Inline title/subtitle with SegmentedPicker +VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { + Text("Camera") + .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) + .foregroundStyle(.white) + + Text("Choose between front and back camera lenses") + .font(.system(size: Design.BaseFontSize.caption)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + + SegmentedPicker( + title: "", + options: [("Front", .front), ("Back", .back)], + selection: $cameraPosition + ) +} + +// ✅ AFTER: Use SettingsSegmentedPicker +SettingsSegmentedPicker( + title: "Camera", + subtitle: "Choose between front and back camera lenses", + options: [("Front", .front), ("Back", .back)], + selection: $cameraPosition +) +``` + +--- + +### SegmentedPicker (Low-level) + +A horizontal capsule-style picker without title/subtitle styling. Use `SettingsSegmentedPicker` for settings screens. ```swift SegmentedPicker( @@ -708,7 +768,8 @@ SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { | `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` | +| `SettingsSegmentedPicker` | Option selection | `title`, `subtitle`, `options`, `titleAccessory` | +| `SegmentedPicker` | Low-level picker | `title`, `options`, `selection` | | `SettingsRow` | Action rows | `systemImage`, `title`, `action` | | `SelectableRow` | Card selection | `title`, `isSelected`, `badge` | | `BadgePill` | Price/tag badges | `text`, `isSelected` | diff --git a/Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift b/Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift new file mode 100644 index 0000000..de982b8 --- /dev/null +++ b/Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift @@ -0,0 +1,173 @@ +// +// SettingsSegmentedPicker.swift +// Bedrock +// +// A segmented picker with title, subtitle, and optional accessory. +// + +import SwiftUI + +/// A segmented picker setting with title, subtitle, and capsule-style buttons. +/// +/// Use this for selecting from a small number of options (2-4) in settings. +/// +/// ```swift +/// // Basic picker +/// SettingsSegmentedPicker( +/// title: "Camera", +/// subtitle: "Choose between front and back camera", +/// options: [("Front", .front), ("Back", .back)], +/// selection: $cameraPosition +/// ) +/// +/// // Premium picker with crown icon +/// SettingsSegmentedPicker( +/// title: "HDR Mode", +/// subtitle: "High Dynamic Range for better lighting", +/// options: [("Off", .off), ("On", .on), ("Auto", .auto)], +/// selection: $hdrMode, +/// titleAccessory: { +/// Image(systemName: "crown.fill") +/// .foregroundStyle(.orange) +/// } +/// ) +/// ``` +public struct SettingsSegmentedPicker: View { + /// The main title text. + public let title: String + + /// The subtitle/description text. + public let subtitle: String + + /// The available options as (label, value) pairs. + public let options: [(String, T)] + + /// Binding to the selected value. + @Binding public var selection: T + + /// The accent color for the selected button. + public let accentColor: Color + + /// Optional accessory view shown after the title (e.g., crown icon for premium). + public let titleAccessory: Accessory? + + /// Creates a settings segmented picker. + /// - Parameters: + /// - title: The main title. + /// - subtitle: The subtitle description. + /// - options: Array of (label, value) tuples. + /// - selection: Binding to selected value. + /// - accentColor: The accent color (default: primary accent). + /// - titleAccessory: Optional view after title (e.g., premium crown). + public init( + title: String, + subtitle: String, + options: [(String, T)], + selection: Binding, + accentColor: Color = .Accent.primary, + @ViewBuilder titleAccessory: () -> Accessory? = { nil as EmptyView? } + ) { + self.title = title + self.subtitle = subtitle + self.options = options + self._selection = selection + self.accentColor = accentColor + self.titleAccessory = titleAccessory() + } + + public var body: some View { + VStack(alignment: .leading, spacing: Design.Spacing.small) { + // Title row with optional accessory + 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)) + } + } + + // Subtitle + Text(subtitle) + .font(.system(size: Design.BaseFontSize.caption)) + .foregroundStyle(.white.opacity(Design.Opacity.medium)) + + // Segmented buttons + HStack(spacing: Design.Spacing.small) { + ForEach(options.indices, id: \.self) { index in + let option = options[index] + Button { + selection = option.1 + } label: { + Text(option.0) + .font(.system(size: Design.BaseFontSize.body, weight: .medium)) + .foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong)) + .padding(.vertical, Design.Spacing.small) + .frame(maxWidth: .infinity) + .background( + Capsule() + .fill(selection == option.1 ? accentColor : Color.white.opacity(Design.Opacity.subtle)) + ) + } + .buttonStyle(.plain) + } + } + } + .padding(.vertical, Design.Spacing.xSmall) + } +} + +// MARK: - Convenience Initializer + +extension SettingsSegmentedPicker where Accessory == EmptyView { + /// Creates a settings segmented picker without a title accessory. + public init( + title: String, + subtitle: String, + options: [(String, T)], + selection: Binding, + accentColor: Color = .Accent.primary + ) { + self.title = title + self.subtitle = subtitle + self.options = options + self._selection = selection + self.accentColor = accentColor + self.titleAccessory = nil + } +} + +// MARK: - Preview + +#Preview { + VStack(spacing: Design.Spacing.large) { + SettingsSegmentedPicker( + title: "Camera", + subtitle: "Choose between front and back camera lenses", + options: [("Front", "front"), ("Back", "back")], + selection: .constant("front") + ) + + SettingsSegmentedPicker( + title: "Flash Mode", + subtitle: "Controls automatic flash behavior for photos", + options: [("Off", 0), ("On", 1), ("Auto", 2)], + selection: .constant(0) + ) + + SettingsSegmentedPicker( + title: "HDR Mode", + subtitle: "High Dynamic Range for better lighting in photos", + options: [("Off", "off"), ("On", "on"), ("Auto", "auto")], + selection: .constant("off"), + titleAccessory: { + Image(systemName: "crown.fill") + .foregroundStyle(.orange) + } + ) + } + .padding() + .background(Color.Surface.overlay) +}