Files
workouts/Workouts/Seed/SeedLibrary.swift
T
rzen 6e440317c4 Restructure into a three-tab app with Progress, goals, and Meditation
The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView
becomes a Today / Progress / Settings TabView, "Routine" replaces
"Split" in every user-facing string and view name (code-level types
keep their names), and workout starting moves to shared
WorkoutStarter / StartedWorkoutNavigator plumbing.

- New Progress tab: weekly goal streaks, workout trends, per-exercise
  weight progression, achievements, and the full history list
  (WorkoutLogsView -> WorkoutHistoryView).
- Goals: stable categories workouts roll up to, managed from Settings.
- New Meditation exercise + starter routine; timed sits record to
  Apple Health as Mind & Body sessions.

Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
2026-07-11 07:53:01 -04:00

70 lines
3.5 KiB
Swift

import Foundation
import IndieSync
/// The bundled immutable starter-routine library. Each seed ships as byte-canonical
/// `RoutineDocument` JSON in `Resources/StarterSplits/*.split.json` (the resource
/// directory and file extension keep their legacy "split" spelling — bundled asset
/// names are pinned, see below), keyed by a fixed ULID (the shared `01DXF6DT00`
/// prefix, minted from a frozen 2020 timestamp) so every install and device agrees
/// on the same identity.
///
/// Seeds are never mutated in place: editing one clones it to a fresh random ULID
/// and soft-deletes the seed (see `SyncEngine.save(routine:)`). The seed's tombstone
/// is exempt from pruning, so it vetoes resurrection forever; the app bundle — not
/// the stub — is the canonical restore source, since the seed content is immutable.
enum SeedLibrary {
/// One bundled seed: its fixed id, decoded document, and the verbatim bundle
/// bytes. The bytes are written to iCloud unchanged so the same seed is
/// byte-identical on every device — a same-path conflict between two devices
/// seeding at once is then semantically empty (both wrote the same bytes).
struct Seed: Sendable {
let id: String
let doc: RoutineDocument
let data: Data
}
/// Resolves the enclosing bundle in both the app and a hosted XCTest bundle
/// (whose `Bundle.main` is the test runner, not the app), where the flattened
/// resources actually live.
private final class BundleToken {}
/// Every bundled seed, decoded once and cached, in display order.
static let seeds: [Seed] = {
let bundle = Bundle(for: BundleToken.self)
// XcodeGen flattens resource groups, so the files land in the bundle root as
// "<Name>.split.json" — enumerate all json and filter on the compound suffix.
// PINNED: the bundled resource extension stays ".split.json" (asset names in
// StarterSplits/ are shipped files, not symbols — the rename must not touch them).
let urls = (bundle.urls(forResourcesWithExtension: "json", subdirectory: nil) ?? [])
.filter { $0.lastPathComponent.hasSuffix(".split.json") }
let loaded: [Seed] = urls.compactMap { url in
guard let data = try? Data(contentsOf: url),
let doc = try? DocumentCoder.decode(RoutineDocument.self, from: data)
else { return nil }
return Seed(id: doc.id, doc: doc, data: data)
}
return loaded.sorted { $0.doc.order < $1.doc.order }
}()
static let seedIDs: Set<String> = Set(seeds.map(\.id))
static func isSeed(id: String) -> Bool { seedIDs.contains(id) }
static func seed(id: String) -> Seed? { seeds.first { $0.id == id } }
/// True when `doc` is unchanged from its pristine seed in every field except
/// `updatedAt`. The edit sheets stamp `updatedAt` on every Save, so a no-op save
/// would otherwise read as an edit and fork the seed; normalizing both timestamps
/// to a common value before the `==` isolates real changes. A non-seed id has no
/// seed to compare against — treated as pristine (callers gate on `isSeed` first).
static func isPristine(_ doc: RoutineDocument) -> Bool {
guard let seed = seed(id: doc.id) else { return true }
var edited = doc
var pristine = seed.doc
let common = Date(timeIntervalSince1970: 0)
edited.updatedAt = common
pristine.updatedAt = common
return edited == pristine
}
}