Bedrock/Sources/Bedrock/Views/Settings/iCloudSyncSettingsView.swift

193 lines
6.3 KiB
Swift

//
// 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(.subheadline)
.foregroundStyle(syncStatusColor)
Text(syncStatusText)
.font(.caption)
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Spacer()
Button {
viewModel.forceSync()
} label: {
Text(String(localized: "Sync Now"))
.font(.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)
}