Add CloudSyncable protocol and iCloudSyncSettingsView for reusable iCloud sync section
This commit is contained in:
parent
3e1d63740f
commit
41b3174c3b
@ -612,6 +612,57 @@ BadgePill(
|
||||
|
||||
---
|
||||
|
||||
## iCloud Sync Section
|
||||
|
||||
Bedrock provides a reusable iCloud sync section with the `CloudSyncable` protocol and `iCloudSyncSettingsView`.
|
||||
|
||||
### Step 1: Conform ViewModel to CloudSyncable
|
||||
|
||||
```swift
|
||||
import Bedrock
|
||||
|
||||
@Observable @MainActor
|
||||
final class SettingsViewModel: CloudSyncable {
|
||||
private let cloudSync = CloudSyncManager<SyncedSettings>()
|
||||
|
||||
// CloudSyncable protocol requirements
|
||||
var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
|
||||
var iCloudEnabled: Bool {
|
||||
get { cloudSync.iCloudEnabled }
|
||||
set { cloudSync.iCloudEnabled = newValue }
|
||||
}
|
||||
var lastSyncDate: Date? { cloudSync.lastSyncDate }
|
||||
var syncStatus: String { cloudSync.syncStatus }
|
||||
var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
|
||||
|
||||
func forceSync() { cloudSync.sync() }
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Use iCloudSyncSettingsView
|
||||
|
||||
```swift
|
||||
SettingsSectionHeader(title: "iCloud Sync", systemImage: "icloud", accentColor: AppAccent.primary)
|
||||
|
||||
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
iCloudSyncSettingsView(
|
||||
viewModel: viewModel,
|
||||
accentColor: AppAccent.primary,
|
||||
successColor: AppStatus.success,
|
||||
warningColor: AppStatus.warning
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The component automatically handles:
|
||||
- Toggle for enabling/disabling sync
|
||||
- Dynamic subtitle based on iCloud availability
|
||||
- Sync status display with appropriate icons
|
||||
- "Sync Now" button
|
||||
- Relative time formatting for last sync date
|
||||
|
||||
---
|
||||
|
||||
## Debug Section Pattern
|
||||
|
||||
Add a debug section to settings for developer tools. This section is only visible in DEBUG builds.
|
||||
@ -773,6 +824,7 @@ SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
| `SettingsRow` | Action rows | `systemImage`, `title`, `action` |
|
||||
| `SelectableRow` | Card selection | `title`, `isSelected`, `badge` |
|
||||
| `BadgePill` | Price/tag badges | `text`, `isSelected` |
|
||||
| `iCloudSyncSettingsView` | iCloud sync controls | `viewModel: CloudSyncable`, colors |
|
||||
|
||||
---
|
||||
|
||||
|
||||
192
Sources/Bedrock/Views/Settings/iCloudSyncSettingsView.swift
Normal file
192
Sources/Bedrock/Views/Settings/iCloudSyncSettingsView.swift
Normal file
@ -0,0 +1,192 @@
|
||||
//
|
||||
// iCloudSyncSettingsView.swift
|
||||
// Bedrock
|
||||
//
|
||||
// A reusable iCloud sync settings section.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - CloudSyncable Protocol
|
||||
|
||||
/// Protocol for view models that support iCloud sync.
|
||||
///
|
||||
/// Conform your settings view model to this protocol to use `iCloudSyncSettingsView`.
|
||||
///
|
||||
/// ```swift
|
||||
/// @Observable @MainActor
|
||||
/// class SettingsViewModel: CloudSyncable {
|
||||
/// private let cloudSync = CloudSyncManager<SyncedSettings>()
|
||||
///
|
||||
/// var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
|
||||
/// var iCloudEnabled: Bool {
|
||||
/// get { cloudSync.iCloudEnabled }
|
||||
/// set { cloudSync.iCloudEnabled = newValue }
|
||||
/// }
|
||||
/// var lastSyncDate: Date? { cloudSync.lastSyncDate }
|
||||
/// var syncStatus: String { cloudSync.syncStatus }
|
||||
/// var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
|
||||
///
|
||||
/// func forceSync() { cloudSync.sync() }
|
||||
/// }
|
||||
/// ```
|
||||
@MainActor
|
||||
public protocol CloudSyncable: AnyObject, Observable {
|
||||
/// Whether iCloud sync is available on this device.
|
||||
var iCloudAvailable: Bool { get }
|
||||
|
||||
/// Whether iCloud sync is enabled by the user.
|
||||
var iCloudEnabled: Bool { get set }
|
||||
|
||||
/// The last sync date, if available.
|
||||
var lastSyncDate: Date? { get }
|
||||
|
||||
/// Current sync status message.
|
||||
var syncStatus: String { get }
|
||||
|
||||
/// Whether the initial sync has completed.
|
||||
var hasCompletedInitialSync: Bool { get }
|
||||
|
||||
/// Forces an immediate sync with iCloud.
|
||||
func forceSync()
|
||||
}
|
||||
|
||||
// MARK: - iCloud Sync Settings View
|
||||
|
||||
/// A reusable iCloud sync settings section.
|
||||
///
|
||||
/// Use this in settings screens to provide iCloud sync controls.
|
||||
///
|
||||
/// ```swift
|
||||
/// SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
/// iCloudSyncSettingsView(
|
||||
/// viewModel: viewModel,
|
||||
/// accentColor: AppAccent.primary,
|
||||
/// successColor: AppStatus.success,
|
||||
/// warningColor: AppStatus.warning
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
public struct iCloudSyncSettingsView<ViewModel: CloudSyncable>: View {
|
||||
/// The view model conforming to CloudSyncable.
|
||||
@Bindable public var viewModel: ViewModel
|
||||
|
||||
/// The accent color for interactive elements.
|
||||
public let accentColor: Color
|
||||
|
||||
/// The color for success states.
|
||||
public let successColor: Color
|
||||
|
||||
/// The color for warning/syncing states.
|
||||
public let warningColor: Color
|
||||
|
||||
/// Creates an iCloud sync settings view.
|
||||
/// - Parameters:
|
||||
/// - viewModel: The view model conforming to CloudSyncable.
|
||||
/// - accentColor: Accent color for toggle and buttons (default: primary accent).
|
||||
/// - successColor: Color for synced state (default: success status).
|
||||
/// - warningColor: Color for syncing state (default: warning status).
|
||||
public init(
|
||||
viewModel: ViewModel,
|
||||
accentColor: Color = .Accent.primary,
|
||||
successColor: Color = .Status.success,
|
||||
warningColor: Color = .Status.warning
|
||||
) {
|
||||
self.viewModel = viewModel
|
||||
self.accentColor = accentColor
|
||||
self.successColor = successColor
|
||||
self.warningColor = warningColor
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
// Sync toggle
|
||||
SettingsToggle(
|
||||
title: String(localized: "Sync Settings"),
|
||||
subtitle: viewModel.iCloudAvailable
|
||||
? String(localized: "Sync settings across all your devices")
|
||||
: String(localized: "Sign in to iCloud to enable sync"),
|
||||
isOn: $viewModel.iCloudEnabled,
|
||||
accentColor: accentColor
|
||||
)
|
||||
.disabled(!viewModel.iCloudAvailable)
|
||||
.accessibilityHint(String(localized: "Syncs settings across all your devices via iCloud"))
|
||||
|
||||
// Sync status (show when enabled and available)
|
||||
if viewModel.iCloudEnabled && viewModel.iCloudAvailable {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: syncStatusIcon)
|
||||
.font(.system(size: Design.BaseFontSize.body))
|
||||
.foregroundStyle(syncStatusColor)
|
||||
|
||||
Text(syncStatusText)
|
||||
.font(.system(size: Design.BaseFontSize.caption))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
viewModel.forceSync()
|
||||
} label: {
|
||||
Text(String(localized: "Sync Now"))
|
||||
.font(.system(size: Design.BaseFontSize.caption, weight: .medium))
|
||||
.foregroundStyle(accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.top, Design.Spacing.xSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sync Status Helpers
|
||||
|
||||
private var syncStatusIcon: String {
|
||||
if !viewModel.hasCompletedInitialSync {
|
||||
return "arrow.triangle.2.circlepath"
|
||||
}
|
||||
return viewModel.syncStatus.isEmpty ? "checkmark.icloud" : "icloud"
|
||||
}
|
||||
|
||||
private var syncStatusColor: Color {
|
||||
if !viewModel.hasCompletedInitialSync {
|
||||
return warningColor
|
||||
}
|
||||
return successColor
|
||||
}
|
||||
|
||||
private var syncStatusText: String {
|
||||
if !viewModel.hasCompletedInitialSync {
|
||||
return String(localized: "Syncing...")
|
||||
}
|
||||
|
||||
if let lastSync = viewModel.lastSyncDate {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .abbreviated
|
||||
return String(localized: "Last synced \(formatter.localizedString(for: lastSync, relativeTo: Date()))")
|
||||
}
|
||||
|
||||
return viewModel.syncStatus.isEmpty
|
||||
? String(localized: "Synced")
|
||||
: viewModel.syncStatus
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
// Mock view model for preview
|
||||
@Previewable @State var mockEnabled = true
|
||||
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
Text("iCloud Sync Settings")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
// Note: Preview requires a concrete CloudSyncable implementation
|
||||
Text("Use with a CloudSyncable-conforming ViewModel")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.white.opacity(0.6))
|
||||
}
|
||||
.padding()
|
||||
.background(Color.Surface.overlay)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user