Files
workouts/Shared/Persistence/WorkoutsModelContainer.swift
T
rzen 7274f155e9 Add the exercise reference library, animated exercise figures, and exercise categories
Exercise Library/ holds per-exercise reference docs (setup, cues,
mistakes, progressions) with SVG visuals and a Python-rendered motion
pipeline; Workouts/ExerciseFigure renders the bundled *.motion.json
rigs as animated stick figures on the exercise screen. Exercises gain
a warm-up/main-circuit category, timed exercises display hold time via
planSummary, and a completed exercise reopens to a check screen instead
of its timers.
2026-07-06 01:15:52 -04:00

86 lines
3.7 KiB
Swift

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.
/// 2: added health-metric columns to `Workout` and `activityTypeRaw` to `Split`.
/// 3: added `categoryRaw` to `Exercise`.
static let currentSchemaVersion = 3
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)
}
}
}