Replace the in-workout "+" toolbar button with an ellipsis menu offering
"Add Exercise" and "End Workout". Ending opens a Save/Discard action sheet:
Save marks the remaining exercises as skipped and resolves the workout to
completed (stamping end), which drops it off the watch's active list and ends
the watch's HealthKit session; Discard soft-deletes it.
Teach the status-from-logs derivation that a skipped log is terminal, and
consolidate the three duplicated copies into a single shared
WorkoutDocument.recomputeStatusFromLogs() so an ended workout stays finished
regardless of which screen the next edit comes from.
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
Two-way driving only worked watch->phone: the watch's navigated driver
broadcast and the phone auto-presented a follower cover. The reverse
failed on both ends — the phone's in-list ExerciseProgressView never
broadcast (only its cover did), and the watch had no surface to present
an incoming run.
- Wire the live channel into the phone's in-list driver (broadcast +
follow) via a progressView(logID:) helper in WorkoutLogListView.
- Add a watch follower cover (LiveRunCoverView, mirroring the phone's),
presented from ContentView when the phone drives a run the watch isn't
already in; the watch bridge gains presentable / muteLive.
- Add a navigatedRunID guard on both sides so a device already in the run
follows it inline rather than stacking a cover over itself.
Now starting or driving on either device surfaces the run on the other —
as a follower cover when idle, or inline when already in that run — and
either side can take over.
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
The two-way live run unified the propped-phone mirror onto the real
ExerciseProgressView driver, whose phase views anchor their timers to a
local Date() captured when the page becomes active. For a remote-driven
transition that meant anchoring at phaseStart + delivery-latency +
render-time, so the receiver's countdown lagged the driver — visibly so
iPhone->watch, where the watch is the slower receiver (apply -> SwiftUI
re-render -> TabView animation -> phase view activates).
The frame already carries phaseStart/phaseEnd wall-clock anchors (the way
the old read-only mirror counted off them). Honor them: when a phase is
reached by applying a remote frame, the active phase view anchors its
timer to the frame's phaseStart/phaseEnd instead of local now. Scoped to
the remote-driven page (remoteAnchorPage) so a local swipe or auto-advance
still self-anchors; any local transition clears it. Applied symmetrically
to both the iPhone and watch drivers.
Delivery latency no longer shifts the displayed timer in either direction
(limited only by sub-second inter-device clock skew), and the auto-advance
at zero fires together since both share the same phaseEnd.
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
The propped-up iPhone now runs the real ExerciseProgressView for a live
watch workout instead of a read-only mirror, and the live-run channel is
symmetric — either device can drive the flow and the other follows.
Each page transition is classified human / auto / remote: only human
transitions (swipe, Start, One More, swipe-back reset) are broadcast and
recorded by the actor; auto-advances (rest / timed-work countdown) record
locally but aren't sent, since both devices reach them independently off
the shared wall-clock anchors; an applied remote frame jumps the page
without re-recording or re-broadcasting. That rule is also what stops an
echo loop.
- PhoneConnectivityBridge gains sendLiveProgress/sendLiveEnded (the
missing phone->watch direction); WatchConnectivityBridge receives
frames into an observable liveIncoming via a new didReceiveMessage
route. Both share one increasing per-run version sequence so the
stale-frame guard works across the two devices' counters.
- Both ExerciseProgressViews gain an incomingFrame input + applyIncoming
(syncing setCount for a remote One More); the iPhone one gains the
liveSnapshot/broadcast machinery the watch already had.
- New LiveRunCoverView wraps the real driver for the cover (resolves the
workout, persists via SyncEngine, wires the live channel + close);
ContentView presents it; LiveProgressMirrorView is removed.
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
Move the idle-timer disable out of ExerciseView/ExerciseProgressView and
up to the app scene, re-asserting it whenever the scene becomes active
(iOS clears the flag on background). A propped-up phone now stays lit for
the entire workout, not just while an exercise is open.
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
Add an ephemeral live-run presence channel (separate from the durable
iCloud progress sync) so a propped-up iPhone can mirror the Watch's
Ready → work/rest → Finish flow in real time as the user swipes.
Watch drives, phone mirrors (read-only), so there's no echo loop:
- Watch's ExerciseProgressView broadcasts a LiveProgress frame on every
phase transition (and an ended signal on leave) via sendMessage,
reachable-only — throwaway presence, never written to iCloud.
- Timers ride as wall-clock anchors (Date kept native in the WC dict to
preserve sub-second precision), so both devices count independently
off shared start times and stay in lockstep without streaming ticks.
- Phone holds a transient LiveRunState; ContentView auto-presents a
read-only LiveProgressMirrorView full-screen cover while a run is live.
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
Publish an exclusive-edit lock (editingWorkoutID / editingSplitID) in the
phone→watch application context. While the phone has a workout's exercise
(ExerciseView) or a split (SplitDetailView) open in an editor, the watch pops
out of that run, blocks re-entry, and shows it as "Editing on iPhone" — so the
two devices never drive the same run at once and the watch can't clobber the
phone's edit with a stale optimistic write. The lock clears when the editor
closes; absent keys in the latest-wins context mean "not editing".
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
Tapping an in-progress exercise on the watch froze the app in an infinite
SwiftUI re-render loop. WorkoutLogListView.body observed SwiftData two ways
the iPhone list deliberately avoids: a @Query-bound Split, and a traversal of
its `exercises` relationship during render (availableExercises). Reading an
observed model's relationship inside body keeps the view perpetually
subscribed and re-invalidating. Fix: fetch the split imperatively (not via
@Query), gate the Add-Exercise affordances on the value-type doc.splitID, and
evaluate availableExercises only from the picker sheet's closure. The list
body now depends solely on the value-type working doc.
Also remove the temporary on-screen diagnostics/PVDiag plumbing and restore
PhaseTimerLayout and the dot-row animation that were dropped while debugging.
Make the Ready? page always lead the exercise flow on both watch and iPhone
(previously only for not-started exercises), so a resumed run can swipe back
to it. A deliberate swipe back to Ready? resets the run; the transient paged
TabView snap-to-0 on open is guarded by a settle gate plus an adjacent-swipe
check, so an in-progress exercise lands on its set and is never reset on open.
Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
Tapping an exercise now opens ExerciseProgressView -- the watch's Ready -> work/rest -> Finish flow on iPhone, with rep sets counting up, timed sets and rests counting down and auto-advancing, a work-set dot row (dash on the active set, gap widening at the current rest), and capsule Start/Done/One More buttons. The detail/edit screen moves behind a trailing Edit swipe (leading swipe still completes). Swiping back to the Ready page resets the run.
- iPhone: disable the idle timer while the exercise detail screen is open,
so the display no longer sleeps mid-set.
- Watch: the work/rest timers counted on a run-loop Timer that watchOS
throttles in the Always-On (wrist-down) state, so they froze. Anchor both
to a wall-clock Date rendered with SwiftUI's self-updating timer text;
rest haptics + auto-advance now derive from the end time so they catch up
after a stall instead of stalling.
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
ExerciseView now observes workout.updatedAt and pulls the latest doc/progress
from the cache, mirroring WorkoutLogListView. A set completed on the watch now
advances the iPhone set grid in real time instead of only on re-entry.
Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
- Redesign the watch app into an active-workout runner: a root gate shows the
in-progress workout's exercises or prompts to start one on iPhone, and each
exercise runs as a horizontally-paged HIIT cycle (count-up work, count-down
rest with final-three-second haptics + auto-advance, One More / Done on the
last set). Replaces the old history list.
- Add a configurable rest-between-sets duration in iPhone Settings (default 45s),
synced to the watch over WatchConnectivity.
- Launch the watch app into the session when a workout starts on the phone via
HealthKit (startWatchApp); the watch runs an HKWorkoutSession for foreground
runtime and ends it when the workout finishes. Adds the HealthKit entitlement +
Health usage strings on both targets and WKBackgroundModes on the watch.
Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
A row tap pushed twice: a value-based navigationDestination(for: String.self)
collided with the row's NavigationLink(value:), surfacing a duplicate split list
over the hidden exercise detail. Rows now use a destination-based NavigationLink,
leaving navigationDestination(item:) as the view's only destination.
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.
Implement real-time sync between iOS and Apple Watch apps using
WatchConnectivity framework. This replaces reliance on CloudKit
which doesn't work reliably in simulators.
- Add WatchConnectivityManager to both iOS and Watch targets
- Sync workouts, splits, exercises, and logs between devices
- Update iOS views to trigger sync on data changes
- Add onChange observer to ExerciseView for live progress updates
- Configure App Groups for shared container storage
- Add Watch app views: WorkoutLogsView, WorkoutLogListView, ExerciseProgressView
Schema & Models:
- Add notes, loadType, duration fields to WorkoutLog
- Align Watch schema with iOS (use duration Date instead of separate mins/secs)
- Add duration helper properties to Exercise and WorkoutLog
UI Changes:
- Remove Splits and Settings tabs, single Workout Logs view
- Add gear button in nav bar to access Settings as sheet
- Move Splits section into Settings view with inline list
- Redesign ExerciseView with read-only Plan/Notes tiles and Edit buttons
- Add PlanEditView and NotesEditView with Cancel/Save buttons
- Auto-dismiss ExerciseView when completing last set
- Navigate to ExerciseView when adding new exercise
Data Flow:
- Plan edits sync to both WorkoutLog and corresponding Exercise
- Changes propagate up navigation chain via CoreData
- Copy ExerciseView from Workouts_old, adapt for CoreData
- Add WeightProgressionChartView with historical weight data and chart
- Refactor CheckboxListItem to use Button for checkbox tap handling
- Update WorkoutLogListView to use NavigationLink for exercise details
- Checkbox taps now only cycle status, row tap navigates to exercise
- Migrate from SwiftData to CoreData with CloudKit sync
- Add core models: Split, Exercise, Workout, WorkoutLog
- Implement tab-based UI: Workout Logs, Splits, Settings
- Add SF Symbols picker for split icons
- Add exercise picker filtered by split with exclusion of added exercises
- Integrate IndieAbout for settings/about section
- Add Yams for YAML exercise definition parsing
- Include starter exercise libraries (bodyweight, Planet Fitness)
- Add Date extensions for formatting (formattedTime, isSameDay)
- Format workout date ranges to show time-only for same-day end dates
- Add build number update script
- Add app icons