193 lines
6.3 KiB
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)
|
|
}
|