Workouts 2.0: re-base persistence on iCloud Drive documents
Replace Core Data + NSPersistentCloudKitContainer + App-Group store + WatchConnectivity dictionary sync with the QuickRabbit iCloud-documents architecture: - iCloud Drive JSON documents are the sole source of truth (one file per aggregate: Splits/<ULID>.json, Workouts/YYYY/MM/<ULID>.json), with a rebuildable SwiftData cache populated only by an NSMetadataQuery observer and a connect-time reconcile. Soft-delete tombstones; hard iCloud gate. - Shared model layer (ULID, Codable *Documents + stateless mappers, @Model cache entities, SwiftData container) compiled into both targets. - New iPhone<->Watch bridge over WatchConnectivity keyed by ULIDs; the phone is the sole writer of iCloud Drive, the watch round-trips documents. - AppServices DI + iCloud-required root gate; Swift 6 strict concurrency. - Starter splits generated on demand from the bundled YAML catalogs. - Migrate to XcodeGen (project.yml), iOS 26 / watchOS 26; CloudDocuments entitlement (drop CloudKit/App Group/aps-environment). - Duration stored as Int seconds (was a Date epoch hack); fix workout end-on-create, undismissable delete dialog, toolbar-hiding nav stacks, and the Settings placeholder. - Add README/CHANGELOG/LICENSE, .gitignore, refreshed REQUIREMENTS, and the Scripts/ TestFlight pipeline (release.sh + ASC API scripts). MARKETING_VERSION 2.0.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
/// Builds starter Splits from the bundled YAML exercise catalog, grouped by the
|
||||
/// catalog's `split` label (Upper Body / Core / Lower Body). Written on demand
|
||||
/// (never auto-seeded) through the SyncEngine — an empty cache at launch can't be
|
||||
/// told apart from an iCloud library that simply hasn't downloaded yet.
|
||||
enum SplitSeeder {
|
||||
/// Visual theme per starter split group, in display order.
|
||||
private static let groups: [(name: String, color: String, icon: String)] = [
|
||||
("Upper Body", "blue", "figure.strengthtraining.traditional"),
|
||||
("Core", "orange", "figure.core.training"),
|
||||
("Lower Body", "green", "figure.run"),
|
||||
]
|
||||
|
||||
/// Default split source catalog (most universally applicable).
|
||||
private static let sourceFile = "bodyweight-starter.exercises.yaml"
|
||||
|
||||
/// Builds the default split documents (fresh ULIDs each call).
|
||||
static func defaultSplitDocuments() -> [SplitDocument] {
|
||||
let lists = ExerciseListLoader.loadExerciseLists()
|
||||
guard let catalog = lists[sourceFile] else { return [] }
|
||||
|
||||
var docs: [SplitDocument] = []
|
||||
for (order, group) in groups.enumerated() {
|
||||
let items = catalog.exercises.filter { $0.split == group.name }
|
||||
guard !items.isEmpty else { continue }
|
||||
|
||||
let exercises = items.enumerated().map { idx, item in
|
||||
ExerciseDocument(
|
||||
id: ULID.make(), name: item.name, order: idx,
|
||||
sets: 3, reps: 10, weight: 0, loadType: LoadType.weight.rawValue,
|
||||
durationSeconds: 0, weightLastUpdated: nil, weightReminderWeeks: 2
|
||||
)
|
||||
}
|
||||
docs.append(SplitDocument(
|
||||
schemaVersion: SplitDocument.currentSchema, id: ULID.make(),
|
||||
name: group.name, color: group.color, systemImage: group.icon, order: order,
|
||||
createdAt: Date(), updatedAt: Date(), exercises: exercises
|
||||
))
|
||||
}
|
||||
return docs
|
||||
}
|
||||
|
||||
/// Writes any starter splits whose name doesn't already exist, appended after
|
||||
/// existing splits. Idempotent against double-taps / partial prior seeds.
|
||||
@MainActor
|
||||
static func seedDefaults(into context: ModelContext, using sync: SyncEngine) async {
|
||||
let existing = (try? context.fetch(FetchDescriptor<Split>())) ?? []
|
||||
let existingNames = Set(existing.map(\.name))
|
||||
let base = existing.count
|
||||
|
||||
let fresh = defaultSplitDocuments().filter { !existingNames.contains($0.name) }
|
||||
for (offset, var doc) in fresh.enumerated() {
|
||||
doc.order = base + offset
|
||||
await sync.save(split: doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user