import Foundation import SwiftData /// Builds the SwiftData cache container. The iCloud Drive JSON documents are the /// source of truth, so this store is a rebuildable cache — wiping it is always /// safe; `SyncEngine` repopulates it from the container on next launch. enum WorkoutsModelContainer { /// Bump whenever the cache schema changes — wipes and rebuilds from files. static let currentSchemaVersion = 1 private static let schemaVersionKey = "Workouts.persistenceSchemaVersion" private static let identityTokenKey = "Workouts.iCloudIdentityToken" private static var storeURL: URL { URL.applicationSupportDirectory.appending(path: "Workouts.store") } static func make() -> ModelContainer { let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self]) ensureStoreDirectoryExists() wipeIfNeeded() wipeIfAccountChanged() do { // `.none` is load-bearing: the default `.automatic` would silently // enable CloudKit mirroring on top of our iCloud Drive file sync. let config = ModelConfiguration(schema: schema, url: storeURL, cloudKitDatabase: .none) let container = try ModelContainer(for: schema, configurations: [config]) UserDefaults.standard.set(currentSchemaVersion, forKey: schemaVersionKey) return container } catch { print("Workouts: ModelContainer creation failed at \(storeURL.path): \(error). Falling back to in-memory.") } do { let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) return try ModelContainer(for: schema, configurations: [config]) } catch { fatalError("Workouts: could not create even an in-memory ModelContainer: \(error)") } } /// Records the current iCloud identity token as the cache's owner. Call after /// rebuilding the cache for a newly-signed-in account. static func persistCurrentIdentityToken() { UserDefaults.standard.set(currentIdentityTokenData(), forKey: identityTokenKey) } // MARK: - Wipe helpers private static func ensureStoreDirectoryExists() { let parent = storeURL.deletingLastPathComponent() try? FileManager.default.createDirectory(at: parent, withIntermediateDirectories: true) } private static func wipeIfNeeded() { let stored = UserDefaults.standard.integer(forKey: schemaVersionKey) guard stored < currentSchemaVersion else { return } wipeStore() } /// Wipes the cache when the signed-in iCloud account differs from the one the /// cache was built for. A nil token is "not ready yet", not "no account" — so /// only compare once a token resolves; mid-session changes are handled live. private static func wipeIfAccountChanged() { guard let current = currentIdentityTokenData() else { return } let stored = UserDefaults.standard.data(forKey: identityTokenKey) guard current != stored else { return } wipeStore() UserDefaults.standard.set(current, forKey: identityTokenKey) } private static func currentIdentityTokenData() -> Data? { guard let token = FileManager.default.ubiquityIdentityToken else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: false) } private static func wipeStore() { let base = storeURL for url in [base, URL(fileURLWithPath: base.path + "-wal"), URL(fileURLWithPath: base.path + "-shm")] { try? FileManager.default.removeItem(at: url) } } }