Files
workouts/Shared/Screenshots/ScreenshotSeed.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

182 lines
9.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#if DEBUG
import IndieSync
import Foundation
import SwiftData
/// DEBUG-only sample data + launch-arg plumbing for App Store screenshot capture.
/// Activated by launching with `--screenshot`; `--screen <name>` picks which screen
/// the app renders (see each target's screenshot root). Never compiled into release.
enum ScreenshotSeed {
static var isActive: Bool { CommandLine.arguments.contains("--screenshot") }
static func screen(default def: String) -> String {
let args = CommandLine.arguments
if let i = args.firstIndex(of: "--screen"), i + 1 < args.count { return args[i + 1] }
return def
}
/// Insert a believable in-progress workout, a few finished sessions (so charts have
/// a trend), and the starter routines. Returns the in-progress workout to display.
@MainActor
@discardableResult
static func populate(_ context: ModelContext) -> Workout? {
// Idempotent: clear any prior seed so repeated launches don't stack duplicates
// (cascade deletes take the child exercises/logs with them).
try? context.delete(model: Workout.self)
try? context.delete(model: Routine.self)
try? context.delete(model: Schedule.self)
let cal = Calendar(identifier: .gregorian)
let today = Date()
func daysAgo(_ n: Int) -> Date { cal.date(byAdding: .day, value: -n, to: today) ?? today }
// ---- Routines (with exercises) ------------------------------------------
// Names match the bundled Exercise Library rigs so every screen shows its
// animated figure (an unknown name would leave the figure slot empty).
let routines: [(String, String, String, [(String, Int, Int, Int)])] = [
("Upper Body", "blue", "figure.strengthtraining.traditional", [
("Lat Pull Down", 4, 10, 110), ("Seated Row", 4, 10, 90),
("Shoulder Press", 4, 10, 40), ("Arm Curl", 3, 12, 40),
]),
("Lower Body", "green", "figure.run", [
("Leg Press", 4, 10, 140), ("Leg Curl", 4, 10, 40),
("Leg Extension", 4, 10, 80), ("Calfs", 4, 15, 100),
]),
("Core", "orange", "figure.core.training", [
("Plank", 3, 0, 0), ("Abdominal", 4, 10, 40),
("Rotary", 4, 10, 40),
]),
]
var created: [Routine] = []
for (sIndex, s) in routines.enumerated() {
let routine = Routine(id: ULID.make(), name: s.0, color: s.1, systemImage: s.2,
order: sIndex, createdAt: today, updatedAt: today, jsonRelativePath: "")
context.insert(routine)
created.append(routine)
for (eIndex, e) in s.3.enumerated() {
let isDuration = e.0 == "Plank"
let ex = Exercise(id: ULID.make(), name: e.0, order: eIndex, sets: e.1, reps: e.2,
weight: Double(e.3), loadType: (isDuration ? LoadType.duration : .weight).rawValue,
durationTotalSeconds: isDuration ? 45 : 0)
ex.routine = routine
context.insert(ex)
}
}
// ---- Schedules (the Today board's rows, spanning two goal groups) -------
let schedulePlans: [(routine: Routine, goal: GoalKind, recurrence: ScheduleRecurrence,
weekdays: [Int]?, timesPerWeek: Int?)] = [
(created[0], .strength, .fixedDays, [2, 5], nil), // Upper Body · Mon & Thu
(created[1], .strength, .frequency, nil, 2), // Lower Body · 2× per week
(created[2], .mobility, .daily, nil, nil), // Core · daily
]
// Schedules predate the history below, so the Progress tab's adherence
// tracks cover the seeded weeks instead of starting "today".
let scheduledSince = daysAgo(70)
for (i, plan) in schedulePlans.enumerated() {
context.insert(Schedule(
id: ULID.make(), routineID: plan.routine.id, routineName: plan.routine.name,
goalRaw: plan.goal.rawValue, recurrenceRaw: plan.recurrence.rawValue,
weekdays: plan.weekdays, timesPerWeek: plan.timesPerWeek, order: i,
createdAt: scheduledSince, updatedAt: scheduledSince, jsonRelativePath: ""))
}
let upper = routines[0]
let upperID = created[0].id
// ---- Past finished sessions ---------------------------------------------
// Ten weeks of believable history across all three routines so Progress has
// living goal tracks, streaks, trends, and history: Upper Body lands on its
// Mon & Thu slots (one missed), Lower Body twice a week (one missed), Core
// most mornings — unbroken in the last four weeks, so the Mobility streak is
// alive. Upper's Lat Pull Down ramps toward 110 for the progression chart.
// All deterministic — patterns, not randomness.
func addCompleted(routine: Routine, spec: [(String, Int, Int, Int)],
on day: Date, hour: Int, lengthMinutes: Int, latPullDown: Double?) {
let start = cal.date(bySettingHour: hour, minute: 15, second: 0, of: day) ?? day
let end = start.addingTimeInterval(Double(lengthMinutes) * 60)
let workout = Workout(id: ULID.make(), routineID: routine.id, routineName: routine.name,
start: start, end: end, statusRaw: WorkoutStatus.completed.rawValue,
createdAt: start, updatedAt: end, jsonRelativePath: "")
context.insert(workout)
for (eIndex, e) in spec.enumerated() {
let isDuration = e.0 == "Plank"
let weight = e.0 == "Lat Pull Down" ? (latPullDown ?? Double(e.3)) : Double(e.3)
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1,
reps: e.2, weight: isDuration ? 0 : weight,
loadType: (isDuration ? LoadType.duration : .weight).rawValue,
durationTotalSeconds: isDuration ? 45 : 0, currentStateIndex: e.1,
statusRaw: WorkoutStatus.completed.rawValue, notes: nil, date: start,
setEntries: isDuration ? nil
: actualEntries(sets: e.1, reps: e.2, weight: weight, at: start))
log.workout = workout
context.insert(log)
}
}
var upperSlot = 0, lowerSlot = 0
for d in stride(from: 69, through: 1, by: -1) {
let day = daysAgo(d)
let weekday = cal.component(.weekday, from: day)
// Upper Body — Mon & Thu; the 5th slot is the believable miss.
if weekday == 2 || weekday == 5 {
upperSlot += 1
if upperSlot != 5 {
let lift = min(110, 87.5 + 2.5 * Double((upperSlot + 1) / 2))
addCompleted(routine: created[0], spec: routines[0].3,
on: day, hour: 17, lengthMinutes: 40, latPullDown: lift)
}
}
// Lower Body — Tue & Sat, its 2×-per-week rhythm; the 4th slot missed.
if weekday == 3 || weekday == 7 {
lowerSlot += 1
if lowerSlot != 4 {
addCompleted(routine: created[1], spec: routines[1].3,
on: day, hour: 18, lengthMinutes: 35, latPullDown: nil)
}
}
// Core — daily mornings; Sundays skipped beyond the last four weeks.
if d <= 28 || weekday != 1 {
addCompleted(routine: created[2], spec: routines[2].3,
on: day, hour: 6, lengthMinutes: 15, latPullDown: nil)
}
}
// ---- The current in-progress session -----------------------------------
// Carries the routine's id so the Today board's schedule row shows it as
// today's in-progress run.
let workout = Workout(id: ULID.make(), routineID: upperID, routineName: upper.0, start: today,
end: nil, statusRaw: WorkoutStatus.inProgress.rawValue,
createdAt: today, updatedAt: today, jsonRelativePath: "")
context.insert(workout)
// Lat Pull Down done, Seated Row partway, the rest to go.
let progress = [(4, WorkoutStatus.completed), (2, .inProgress), (0, .notStarted), (0, .notStarted)]
for (eIndex, e) in upper.3.enumerated() {
let (idx, status) = progress[eIndex]
let weight = e.0 == "Lat Pull Down" ? 110 : Double(e.3)
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1, reps: e.2,
weight: weight, loadType: LoadType.weight.rawValue,
durationTotalSeconds: 0, currentStateIndex: idx,
statusRaw: status.rawValue, notes: nil, date: today,
setEntries: idx > 0 ? actualEntries(sets: idx, reps: e.2, weight: weight, at: today) : nil)
log.workout = workout
context.insert(log)
}
try? context.save()
return workout
}
/// Plausible per-set actuals for a weighted log: plan values, with the last
/// set a couple of reps short so screenshots show real-looking variation.
private static func actualEntries(sets: Int, reps: Int, weight: Double, at date: Date) -> [SetEntry] {
(0..<sets).map { i in
SetEntry(reps: i == sets - 1 ? max(1, reps - 2) : reps, weight: weight,
completedAt: date.addingTimeInterval(Double(i) * 150))
}
}
}
#endif