Add App Store screenshot harness + listing metadata

DEBUG-only screenshot support (seeded sample data via ScreenshotSeed, per-screen
launch args, screenshot roots for both apps) so iPhone + Apple Watch App Store
shots can be captured deterministically from the simulator — all excluded from
release builds. Also seed ExerciseView's set-grid progress in init so it renders
correctly on the first frame. Adds Scripts/metadata/ as the listing source of
truth (copy, URLs, review notes, and the captured screenshots).

Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
This commit is contained in:
2026-06-19 19:23:54 -04:00
parent 1d856b98d0
commit 84d45a6d41
26 changed files with 313 additions and 16 deletions
+3
View File
@@ -20,6 +20,9 @@ final class AppServices {
self.container = container
self.syncEngine = SyncEngine(container: container)
self.watchBridge = PhoneConnectivityBridge(container: container, syncEngine: syncEngine)
#if DEBUG
if ScreenshotSeed.isActive { ScreenshotSeed.populate(container.mainContext) }
#endif
}
/// Launch step: resolve iCloud and reconcile the cache. Idempotent repeated
@@ -0,0 +1,43 @@
#if DEBUG
import SwiftUI
import SwiftData
/// DEBUG-only root used for App Store screenshot capture. Bypasses the iCloud gate and
/// renders one fully-formed screen (chosen by `--screen`) against the seeded cache, so
/// captures are deterministic with no UI automation. Never compiled into release.
struct ScreenshotRootView: View {
let services: AppServices
private var activeWorkout: Workout? {
let context = services.container.mainContext
var descriptor = FetchDescriptor<Workout>(sortBy: [SortDescriptor(\.start, order: .reverse)])
descriptor.fetchLimit = 25
let workouts = (try? context.fetch(descriptor)) ?? []
return workouts.first { $0.status == .inProgress } ?? workouts.first
}
var body: some View {
content
.environment(services)
.environment(services.syncEngine)
.modelContainer(services.container)
}
@ViewBuilder
private var content: some View {
if let workout = activeWorkout {
switch ScreenshotSeed.screen(default: "workouts") {
case "exercise":
let logID = WorkoutDocument(from: workout).logs.first { $0.exerciseName == "Bench Press" }?.id
NavigationStack { ExerciseView(workout: workout, logID: logID ?? "") }
case "settings":
SettingsView()
default:
NavigationStack { WorkoutLogListView(workout: workout) }
}
} else {
Color(.systemBackground)
}
}
}
#endif
@@ -35,7 +35,11 @@ struct ExerciseView: View {
init(workout: Workout, logID: String, seedDoc: WorkoutDocument? = nil) {
self.workout = workout
self.logID = logID
_doc = State(initialValue: seedDoc ?? WorkoutDocument(from: workout))
let initialDoc = seedDoc ?? WorkoutDocument(from: workout)
_doc = State(initialValue: initialDoc)
// Seed progress from the log so the set grid is correct on the first frame
// (onAppear also refreshes it, but that lags the initial render).
_progress = State(initialValue: initialDoc.logs.first { $0.id == logID }?.currentStateIndex ?? 0)
}
/// The log being edited within the working doc.
+17 -5
View File
@@ -14,11 +14,23 @@ struct WorkoutsApp: App {
var body: some Scene {
WindowGroup {
RootGateView()
.environment(services)
.environment(services.syncEngine)
.modelContainer(services.container)
.task { await services.bootstrap() }
#if DEBUG
if ScreenshotSeed.isActive {
ScreenshotRootView(services: services)
} else {
root
}
#else
root
#endif
}
}
private var root: some View {
RootGateView()
.environment(services)
.environment(services.syncEngine)
.modelContainer(services.container)
.task { await services.bootstrap() }
}
}