Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
358b5ccf9d
commit
111242ba9d
@ -61,7 +61,10 @@ Use this checklist when setting up a new app with Bedrock:
|
||||
- [ ] Use `SettingsToggle`, `SettingsSlider`, `SegmentedPicker` components
|
||||
- [ ] Apply theme colors via `AppSurface`, `AppAccent`, etc.
|
||||
- [ ] Use `SettingsCard` for grouped sections
|
||||
- [ ] Use `SettingsDivider` between rows in cards
|
||||
- [ ] Use `SettingsCardRow` for custom rows inside cards
|
||||
- [ ] Use `SettingsNavigationRow` for navigation links
|
||||
- [ ] Avoid manual horizontal child padding inside `SettingsCard` (card owns row insets)
|
||||
- [ ] Add `#if DEBUG` section for development tools
|
||||
|
||||
### Quick Start Order
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"" : {
|
||||
|
||||
},
|
||||
"%lld." : {
|
||||
"comment" : "A numbered list item with a callout number and accompanying text. The first argument is the number of the item. The second argument is the text describing the item.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
||||
@ -248,6 +248,7 @@ Text("Feature unavailable")
|
||||
// Toggles and interactive elements
|
||||
SettingsToggle(
|
||||
title: "Enable Feature",
|
||||
subtitle: "Turn this feature on or off",
|
||||
isOn: $isEnabled,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
@ -264,6 +265,8 @@ SettingsSectionHeader(
|
||||
)
|
||||
```
|
||||
|
||||
> For settings layout alignment, place rows inside `SettingsCard` and use `SettingsCardRow` + `SettingsDivider` for custom row content (see `SETTINGS_GUIDE.md`).
|
||||
|
||||
### Status Colors
|
||||
|
||||
```swift
|
||||
|
||||
@ -230,6 +230,38 @@ struct SettingsView: View {
|
||||
|
||||
---
|
||||
|
||||
## Layout Contract (Important)
|
||||
|
||||
`SettingsCard` owns horizontal insets for rows inside the card.
|
||||
|
||||
- Do: place `SettingsToggle`, `SettingsSlider`, `SettingsNavigationRow`, and other settings rows directly inside `SettingsCard`.
|
||||
- Do: use `SettingsDivider` between rows.
|
||||
- Do: use `SettingsCardRow` for custom rows (custom `HStack`, `ColorPicker`, status row, etc.).
|
||||
- Don't: add manual `.padding(.horizontal, ...)` to children inside `SettingsCard` unless you intentionally want custom indentation.
|
||||
|
||||
Example:
|
||||
|
||||
```swift
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
SettingsNavigationRow(
|
||||
title: "Color Theme",
|
||||
subtitle: "Custom",
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
ThemeSelectionView()
|
||||
}
|
||||
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
|
||||
SettingsCardRow {
|
||||
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Components
|
||||
|
||||
### SettingsCard
|
||||
@ -243,6 +275,8 @@ SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
}
|
||||
```
|
||||
|
||||
**Inset behavior**: `SettingsCard` provides horizontal inset for card children. Child rows should not add their own horizontal inset by default.
|
||||
|
||||
**Refactoring**: Replace inline card styling with `SettingsCard`:
|
||||
|
||||
```swift
|
||||
@ -282,6 +316,46 @@ SettingsSectionHeader(
|
||||
|
||||
---
|
||||
|
||||
### SettingsDivider
|
||||
|
||||
A divider to separate rows inside `SettingsCard`.
|
||||
|
||||
```swift
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
SettingsToggle(...)
|
||||
SettingsDivider(color: AppBorder.subtle)
|
||||
SettingsToggle(...)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SettingsCardRow
|
||||
|
||||
A wrapper for custom row content inside `SettingsCard`, with consistent vertical rhythm.
|
||||
|
||||
```swift
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
SettingsCardRow {
|
||||
HStack {
|
||||
Text("Preview")
|
||||
Spacer()
|
||||
Text("12:34")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `verticalPadding` when you need a taller custom row:
|
||||
|
||||
```swift
|
||||
SettingsCardRow(verticalPadding: Design.Spacing.medium) {
|
||||
CustomContentView()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SettingsToggle
|
||||
|
||||
A toggle row with title, subtitle, and optional title accessory (e.g., premium crown). **Note: Subtitle is required.**
|
||||
@ -443,7 +517,7 @@ A navigation link row for settings that navigate to detail views.
|
||||
SettingsNavigationRow(
|
||||
title: "Open Source Licenses",
|
||||
subtitle: "Third-party libraries used in this app",
|
||||
backgroundColor: AppSurface.primary
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
LicensesView()
|
||||
}
|
||||
@ -482,12 +556,88 @@ NavigationLink {
|
||||
SettingsNavigationRow(
|
||||
title: "Open Source Licenses",
|
||||
subtitle: "Third-party libraries used in this app",
|
||||
backgroundColor: AppSurface.primary
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
LicensesView()
|
||||
}
|
||||
```
|
||||
|
||||
Use `SettingsSelectionView` for enum/list selection destinations:
|
||||
|
||||
```swift
|
||||
SettingsNavigationRow(
|
||||
title: "Theme",
|
||||
subtitle: selectedThemeName,
|
||||
backgroundColor: .clear
|
||||
) {
|
||||
SettingsSelectionView(
|
||||
selection: $viewModel.theme,
|
||||
options: ThemeOption.allCases,
|
||||
title: "Theme",
|
||||
backgroundColor: AppSurface.primary,
|
||||
cardBackgroundColor: AppSurface.card,
|
||||
cardBorderColor: AppBorder.subtle,
|
||||
accentColor: AppAccent.primary,
|
||||
rowTextColor: AppTextColors.primary,
|
||||
toString: { $0.rawValue }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SettingsSelectionView
|
||||
|
||||
A reusable selection screen for settings destinations.
|
||||
|
||||
```swift
|
||||
SettingsSelectionView(
|
||||
selection: $selection,
|
||||
options: options,
|
||||
title: "Choose Option",
|
||||
backgroundColor: AppSurface.primary,
|
||||
cardBackgroundColor: AppSurface.card,
|
||||
cardBorderColor: AppBorder.subtle,
|
||||
accentColor: AppAccent.primary,
|
||||
rowTextColor: AppTextColors.primary,
|
||||
toString: { $0.displayName }
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SettingsLabelValueRow
|
||||
|
||||
A reusable row for custom label/value content inside `SettingsCard`.
|
||||
|
||||
```swift
|
||||
SettingsCard {
|
||||
SettingsLabelValueRow(title: "Preview") {
|
||||
Text("12:34")
|
||||
.fontDesign(.rounded)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use this instead of custom `HStack { Text; Spacer(); value }` rows to keep row spacing aligned with Bedrock settings components.
|
||||
|
||||
---
|
||||
|
||||
### SettingsTimePicker
|
||||
|
||||
A settings accessory that binds to `HH:mm` string values.
|
||||
|
||||
```swift
|
||||
SettingsCard {
|
||||
SettingsLabelValueRow(title: "Start Time", verticalPadding: Design.Spacing.medium) {
|
||||
SettingsTimePicker(
|
||||
timeString: $viewModel.startTime,
|
||||
accentColor: AppAccent.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### LicensesView
|
||||
@ -868,6 +1018,11 @@ SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
| Component | Use Case | Key Parameters |
|
||||
|-----------|----------|----------------|
|
||||
| `SettingsCard` | Group related settings | `backgroundColor`, `borderColor` |
|
||||
| `SettingsDivider` | Row separators in cards | `color` |
|
||||
| `SettingsCardRow` | Custom row content in cards | `verticalPadding`, `content` |
|
||||
| `SettingsLabelValueRow` | Custom label/value row in cards | `title`, `verticalPadding`, `value` |
|
||||
| `SettingsTimePicker` | HH:mm time picker accessory | `timeString`, `accentColor` |
|
||||
| `SettingsSelectionView` | Selection destination screen | `selection`, `options`, `title`, colors, `toString` |
|
||||
| `SettingsSectionHeader` | Section titles | `title`, `systemImage`, `accentColor` |
|
||||
| `SettingsToggle` | Boolean settings | `title`, `subtitle`, `isOn`, `accentColor` |
|
||||
| `SettingsSlider` | Numeric settings | `value`, `range`, `format`, `accentColor` |
|
||||
|
||||
@ -73,7 +73,6 @@ public struct SegmentedPicker<T: Equatable>: View {
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,8 @@ public struct SettingsCard<Content: View>: View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
content
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.padding(.horizontal, Design.Spacing.xLarge)
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||
.fill(backgroundColor)
|
||||
|
||||
54
Sources/Bedrock/Views/Settings/SettingsCardRow.swift
Normal file
54
Sources/Bedrock/Views/Settings/SettingsCardRow.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// SettingsCardRow.swift
|
||||
// Bedrock
|
||||
//
|
||||
// A lightweight row wrapper for custom content inside SettingsCard.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A lightweight row wrapper for custom content inside a ``SettingsCard``.
|
||||
///
|
||||
/// Use this for custom row content so vertical rhythm stays consistent
|
||||
/// with built-in settings components.
|
||||
public struct SettingsCardRow<Content: View>: View {
|
||||
/// The wrapped row content.
|
||||
@ViewBuilder public let content: Content
|
||||
|
||||
/// Vertical padding applied to the row content.
|
||||
public let verticalPadding: CGFloat
|
||||
|
||||
/// Creates a settings card row wrapper.
|
||||
/// - Parameters:
|
||||
/// - verticalPadding: Vertical padding for row content (default: Spacing.small).
|
||||
/// - content: Row content.
|
||||
public init(
|
||||
verticalPadding: CGFloat = Design.Spacing.small,
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.verticalPadding = verticalPadding
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
content
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, verticalPadding)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
SettingsCard(backgroundColor: .Surface.card, borderColor: .Border.subtle) {
|
||||
SettingsCardRow {
|
||||
HStack {
|
||||
Text("Preview").styled(.subheadingEmphasis)
|
||||
Spacer()
|
||||
Text("12:34").styled(.subheadingEmphasis)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.Surface.overlay)
|
||||
}
|
||||
44
Sources/Bedrock/Views/Settings/SettingsDivider.swift
Normal file
44
Sources/Bedrock/Views/Settings/SettingsDivider.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// SettingsDivider.swift
|
||||
// Bedrock
|
||||
//
|
||||
// A divider used between rows inside SettingsCard.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A divider used between rows inside a ``SettingsCard``.
|
||||
///
|
||||
/// By default this draws edge-to-edge within the card content area.
|
||||
public struct SettingsDivider: View {
|
||||
/// The divider color.
|
||||
public let color: Color
|
||||
|
||||
/// Creates a settings divider.
|
||||
/// - Parameter color: Divider color (default: Border.subtle).
|
||||
public init(color: Color = .Border.subtle) {
|
||||
self.color = color
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Rectangle()
|
||||
.fill(color)
|
||||
.frame(height: Design.LineWidth.thin)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
SettingsCard(backgroundColor: .Surface.card, borderColor: .Border.subtle) {
|
||||
SettingsCardRow {
|
||||
Text("First Row").styled(.subheadingEmphasis)
|
||||
}
|
||||
SettingsDivider()
|
||||
SettingsCardRow {
|
||||
Text("Second Row").styled(.subheadingEmphasis)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.Surface.overlay)
|
||||
}
|
||||
71
Sources/Bedrock/Views/Settings/SettingsLabelValueRow.swift
Normal file
71
Sources/Bedrock/Views/Settings/SettingsLabelValueRow.swift
Normal file
@ -0,0 +1,71 @@
|
||||
//
|
||||
// SettingsLabelValueRow.swift
|
||||
// Bedrock
|
||||
//
|
||||
// A reusable label-value row for custom settings content.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A reusable label-value row for settings cards.
|
||||
///
|
||||
/// This component is intended for custom rows that need the same alignment
|
||||
/// and vertical rhythm as other settings controls.
|
||||
public struct SettingsLabelValueRow<Value: View>: View {
|
||||
public let title: String
|
||||
public let titleTypography: Typography
|
||||
public let titleEmphasis: TextEmphasis
|
||||
public let verticalPadding: CGFloat
|
||||
@ViewBuilder public let value: Value
|
||||
|
||||
/// Creates a label-value row.
|
||||
/// - Parameters:
|
||||
/// - title: The leading title text.
|
||||
/// - titleTypography: Typography style for title.
|
||||
/// - titleEmphasis: Emphasis style for title.
|
||||
/// - verticalPadding: Vertical row padding.
|
||||
/// - value: Trailing value view.
|
||||
public init(
|
||||
title: String,
|
||||
titleTypography: Typography = .subheadingEmphasis,
|
||||
titleEmphasis: TextEmphasis = .primary,
|
||||
verticalPadding: CGFloat = Design.Spacing.small,
|
||||
@ViewBuilder value: () -> Value
|
||||
) {
|
||||
self.title = title
|
||||
self.titleTypography = titleTypography
|
||||
self.titleEmphasis = titleEmphasis
|
||||
self.verticalPadding = verticalPadding
|
||||
self.value = value()
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
SettingsCardRow(verticalPadding: verticalPadding) {
|
||||
HStack {
|
||||
Text(title).styled(titleTypography, emphasis: titleEmphasis)
|
||||
Spacer()
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
SettingsCard {
|
||||
SettingsLabelValueRow(title: "Preview") {
|
||||
Text("12:34")
|
||||
.styled(.headingEmphasis)
|
||||
.fontDesign(.rounded)
|
||||
}
|
||||
|
||||
SettingsDivider()
|
||||
|
||||
SettingsLabelValueRow(title: "Current Brightness", verticalPadding: Design.Spacing.medium) {
|
||||
Text("74%").styled(.subheading, emphasis: .secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.Surface.overlay)
|
||||
}
|
||||
@ -77,11 +77,9 @@ public struct SettingsNavigationRow<Destination: View>: View {
|
||||
SymbolIcon.chevron(color: .secondary)
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.background(backgroundColor, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +112,6 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
||||
.sensoryFeedback(.selection, trigger: selection)
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
123
Sources/Bedrock/Views/Settings/SettingsSelectionView.swift
Normal file
123
Sources/Bedrock/Views/Settings/SettingsSelectionView.swift
Normal file
@ -0,0 +1,123 @@
|
||||
//
|
||||
// SettingsSelectionView.swift
|
||||
// Bedrock
|
||||
//
|
||||
// A reusable selection screen for settings navigation rows.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A reusable selection screen for settings values.
|
||||
///
|
||||
/// Use this as the destination for `SettingsNavigationRow` when a setting
|
||||
/// requires choosing one value from a list.
|
||||
public struct SettingsSelectionView<T: Hashable>: View {
|
||||
@Binding public var selection: T
|
||||
public let options: [T]
|
||||
public let title: String
|
||||
public let toString: (T) -> String
|
||||
public let backgroundColor: Color
|
||||
public let cardBackgroundColor: Color
|
||||
public let cardBorderColor: Color
|
||||
public let accentColor: Color
|
||||
public let rowTextColor: Color
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
/// Creates a settings selection view.
|
||||
/// - Parameters:
|
||||
/// - selection: The selected value binding.
|
||||
/// - options: Available options.
|
||||
/// - title: Screen and section title.
|
||||
/// - backgroundColor: Screen background color.
|
||||
/// - cardBackgroundColor: Card background color.
|
||||
/// - cardBorderColor: Card border color.
|
||||
/// - accentColor: Accent color for icon/checkmark.
|
||||
/// - rowTextColor: Row text color.
|
||||
/// - toString: Option display text formatter.
|
||||
public init(
|
||||
selection: Binding<T>,
|
||||
options: [T],
|
||||
title: String,
|
||||
backgroundColor: Color = .Surface.primary,
|
||||
cardBackgroundColor: Color = .Surface.card,
|
||||
cardBorderColor: Color = .Border.subtle,
|
||||
accentColor: Color = .Accent.primary,
|
||||
rowTextColor: Color = .primary,
|
||||
toString: @escaping (T) -> String
|
||||
) {
|
||||
self._selection = selection
|
||||
self.options = options
|
||||
self.title = title
|
||||
self.backgroundColor = backgroundColor
|
||||
self.cardBackgroundColor = cardBackgroundColor
|
||||
self.cardBorderColor = cardBorderColor
|
||||
self.accentColor = accentColor
|
||||
self.rowTextColor = rowTextColor
|
||||
self.toString = toString
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
backgroundColor.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
SettingsSectionHeader(
|
||||
title: title,
|
||||
systemImage: "checklist",
|
||||
accentColor: accentColor
|
||||
)
|
||||
|
||||
SettingsCard(backgroundColor: cardBackgroundColor, borderColor: cardBorderColor) {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(options, id: \.self) { option in
|
||||
Button {
|
||||
selection = option
|
||||
dismiss()
|
||||
} label: {
|
||||
SettingsCardRow(verticalPadding: Design.Spacing.medium) {
|
||||
HStack {
|
||||
Text(toString(option))
|
||||
.styled(.body, emphasis: .custom(rowTextColor))
|
||||
Spacer()
|
||||
if selection == option {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(accentColor)
|
||||
.font(.body.bold())
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.clear)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if option != options.last {
|
||||
SettingsDivider(color: cardBorderColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
.padding(.top, Design.Spacing.large)
|
||||
.padding(.bottom, Design.Spacing.xxxLarge)
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
SettingsSelectionView(
|
||||
selection: .constant("Option 1"),
|
||||
options: ["Option 1", "Option 2", "Option 3"],
|
||||
title: "Test Selection",
|
||||
toString: { $0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -142,7 +142,6 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
85
Sources/Bedrock/Views/Settings/SettingsTimePicker.swift
Normal file
85
Sources/Bedrock/Views/Settings/SettingsTimePicker.swift
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// SettingsTimePicker.swift
|
||||
// Bedrock
|
||||
//
|
||||
// A time picker accessory that binds to an HH:mm string.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A time picker accessory that binds to a 24-hour `HH:mm` string.
|
||||
///
|
||||
/// Useful in settings rows where model persistence is string-based.
|
||||
public struct SettingsTimePicker: View {
|
||||
@Binding public var timeString: String
|
||||
@State private var selectedTime = Date()
|
||||
|
||||
public let accentColor: Color
|
||||
|
||||
/// Creates a settings time picker.
|
||||
/// - Parameters:
|
||||
/// - timeString: Binding to a string in `HH:mm` format.
|
||||
/// - accentColor: Tint color for the picker.
|
||||
public init(
|
||||
timeString: Binding<String>,
|
||||
accentColor: Color = .Accent.primary
|
||||
) {
|
||||
self._timeString = timeString
|
||||
self.accentColor = accentColor
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute)
|
||||
.labelsHidden()
|
||||
.tint(accentColor)
|
||||
.onAppear {
|
||||
updateSelectedTimeFromString()
|
||||
}
|
||||
.onChange(of: selectedTime) { _, newTime in
|
||||
updateStringFromTime(newTime)
|
||||
}
|
||||
.onChange(of: timeString) { _, _ in
|
||||
updateSelectedTimeFromString()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSelectedTimeFromString() {
|
||||
let components = timeString.split(separator: ":")
|
||||
guard components.count == 2,
|
||||
let hour = Int(components[0]),
|
||||
let minute = Int(components[1]) else {
|
||||
return
|
||||
}
|
||||
|
||||
let calendar = Calendar.current
|
||||
let now = Date()
|
||||
let dateComponents = calendar.dateComponents([.year, .month, .day], from: now)
|
||||
|
||||
var newComponents = dateComponents
|
||||
newComponents.hour = hour
|
||||
newComponents.minute = minute
|
||||
|
||||
if let newDate = calendar.date(from: newComponents) {
|
||||
selectedTime = newDate
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStringFromTime(_ time: Date) {
|
||||
let calendar = Calendar.current
|
||||
let hour = calendar.component(.hour, from: time)
|
||||
let minute = calendar.component(.minute, from: time)
|
||||
timeString = String(format: "%02d:%02d", hour, minute)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
SettingsCard {
|
||||
SettingsLabelValueRow(title: "Start Time") {
|
||||
SettingsTimePicker(timeString: .constant("22:00"))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.Surface.overlay)
|
||||
}
|
||||
@ -91,7 +91,6 @@ public struct SettingsToggle<Accessory: View>: View {
|
||||
}
|
||||
.tint(accentColor)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
.sensoryFeedback(.impact(flexibility: .soft), trigger: isOn)
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +151,6 @@ private struct SyncStatusRow<ViewModel: CloudSyncable>: View {
|
||||
Text(String(localized: "Sync Now")).styled(.captionEmphasis, emphasis: .custom(accentColor))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.small)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user