Files
Ascently/ios/Ascently/Services/SyncService.swift

223 lines
6.3 KiB
Swift

import Combine
import Foundation
@MainActor
class SyncService: ObservableObject {
@Published var isSyncing = false
@Published var lastSyncTime: Date?
@Published var syncError: String?
@Published var isConnected = false
@Published var isTesting = false
@Published var isOfflineMode = false
@Published var providerType: SyncProviderType = .server {
didSet {
updateActiveProvider()
userDefaults.set(providerType.rawValue, forKey: Keys.providerType)
}
}
private var activeProvider: SyncProvider?
private let userDefaults = UserDefaults.standard
private let logTag = "SyncService"
private var syncTask: Task<Void, Never>?
private enum Keys {
static let serverURL = "sync_server_url"
static let authToken = "sync_auth_token"
static let lastSyncTime = "last_sync_time"
static let isConnected = "is_connected"
static let autoSyncEnabled = "auto_sync_enabled"
static let offlineMode = "offline_mode"
static let providerType = "sync_provider_type"
}
// Legacy properties for compatibility with SettingsView
var serverURL: String {
get { userDefaults.string(forKey: Keys.serverURL) ?? "" }
set {
userDefaults.set(newValue, forKey: Keys.serverURL)
// If active provider is server, it will pick up the change from UserDefaults
}
}
var authToken: String {
get { userDefaults.string(forKey: Keys.authToken) ?? "" }
set { userDefaults.set(newValue, forKey: Keys.authToken) }
}
var isConfigured: Bool {
return activeProvider?.isConfigured ?? false
}
var isAutoSyncEnabled: Bool {
get { userDefaults.bool(forKey: Keys.autoSyncEnabled) }
set { userDefaults.set(newValue, forKey: Keys.autoSyncEnabled) }
}
init() {
isConnected = userDefaults.bool(forKey: Keys.isConnected)
isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true
isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode)
if let savedType = userDefaults.string(forKey: Keys.providerType),
let type = SyncProviderType(rawValue: savedType) {
self.providerType = type
} else {
self.providerType = .server // Default
}
updateActiveProvider()
}
private func updateActiveProvider() {
switch providerType {
case .server:
activeProvider = ServerSyncProvider()
case .iCloud:
activeProvider = ICloudSyncProvider()
case .none:
activeProvider = nil
}
// Update status based on new provider
if let provider = activeProvider {
isConnected = provider.isConnected
lastSyncTime = provider.lastSyncTime
} else {
isConnected = false
lastSyncTime = nil
}
}
func syncWithServer(dataManager: ClimbingDataManager) async throws {
if isOfflineMode {
AppLogger.info("Sync skipped: Offline mode is enabled.", tag: logTag)
return
}
guard let provider = activeProvider else {
if providerType == .none {
return
}
throw SyncError.notConfigured
}
guard provider.isConfigured else {
throw SyncError.notConfigured
}
// For server provider, we check connection. For others, maybe not needed or different check.
if providerType == .server && !provider.isConnected {
throw SyncError.notConnected
}
isSyncing = true
syncError = nil
defer {
isSyncing = false
}
do {
try await provider.sync(dataManager: dataManager)
// Update last sync time
self.lastSyncTime = provider.lastSyncTime
} catch {
syncError = error.localizedDescription
throw error
}
}
func testConnection() async throws {
guard let provider = activeProvider else {
AppLogger.error("Test connection failed: No active provider", tag: logTag)
throw SyncError.notConfigured
}
isTesting = true
defer { isTesting = false }
try await provider.testConnection()
isConnected = provider.isConnected
userDefaults.set(isConnected, forKey: Keys.isConnected)
}
func triggerAutoSync(dataManager: ClimbingDataManager) {
guard isConnected && isConfigured && isAutoSyncEnabled else {
if isSyncing {
isSyncing = false
}
return
}
guard !isSyncing else { return }
syncTask?.cancel()
syncTask = Task {
try? await Task.sleep(for: .seconds(2))
guard !Task.isCancelled else { return }
do {
try await syncWithServer(dataManager: dataManager)
} catch {
self.isSyncing = false
}
}
}
func forceSyncNow(dataManager: ClimbingDataManager) {
guard isConnected && isConfigured else { return }
syncTask?.cancel()
syncTask = nil
Task {
do {
try await syncWithServer(dataManager: dataManager)
} catch {
self.isSyncing = false
}
}
}
func disconnect() {
activeProvider?.disconnect()
syncTask?.cancel()
syncTask = nil
isSyncing = false
isConnected = false
lastSyncTime = nil
syncError = nil
// These are shared keys, so clearing them affects all providers if they use them
// But disconnect() is usually user initiated action
userDefaults.set(false, forKey: Keys.isConnected)
}
func clearConfiguration() {
serverURL = ""
authToken = ""
lastSyncTime = nil
isConnected = false
isAutoSyncEnabled = true
userDefaults.removeObject(forKey: Keys.lastSyncTime)
userDefaults.removeObject(forKey: Keys.isConnected)
userDefaults.removeObject(forKey: Keys.autoSyncEnabled)
syncTask?.cancel()
syncTask = nil
activeProvider?.disconnect()
}
deinit {
syncTask?.cancel()
}
}