TestFlight 2.3 (125) "crashed when watch ended an exercise": the
isDeleted guard from 85e1582 only covers the delete→save window. Once
the deletion is saved the model unregisters — isDeleted reads false
again, modelContext goes nil, and any persisted-property read still
traps (_InitialBackingData.getValue). StartedWorkoutNavigator retained
the run's @Model in @State for the whole workout, so an observer
remove/re-add churn (e.g. iCloud reachability flapping) invalidated it
underneath the pushed screen, and the watch's completion push triggered
the rebuild that read it.
Two layers: the fromLive/SplitDetailView guards now also require a
non-nil modelContext, and StartedWorkoutNavigator pushes a plain id
route, re-fetching the entity fresh on every destination build — a
re-imported run resolves to its live instance; a gone run shows a
placeholder instead of trapping.
Claude-Session: https://claude.ai/code/session_01PVNBVKp5bcq52X722uMjwT
A closure-based NavigationLink builds its destination view eagerly for
every row on every parent-list body evaluation — including the view-graph
update SwiftUI runs the instant SyncEngine.delete does context.delete +
save. Mapping the row's just-deleted @Model to a document there reads a
persisted property on a dead model, which traps in SwiftData
(_InitialBackingData.getValue -> assertionFailure). This is the "Crashed
when deleted a workout" TestFlight report on 2.3 (124).
Add WorkoutDocument(fromLive:) (nil when isDeleted) + a .deletedPlaceholder
and route every destination-init / updatedAt-absorb map through it:
phone WorkoutLogListView + SplitDetailView (read split.id) and watch
WorkoutLogListView. Fixes the whole crash class, not just the report.
Claude-Session: https://claude.ai/code/session_01BQcEWmAPA78338QuEwRkAh
Pick any exercise from the full library while a workout is running, not
just the ones in its split. The new exercise's plan is seeded from the
most recent log of that exercise, else the library's authored Defaults
line, else a plain 3x10. Adds a searchable two-section picker sheet and
a **Defaults:** bullet to all 47 library reference pages.
Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
Machine-based exercises now carry ordered name/value comfort settings
(seat height, back-rest position, ...) on both ExerciseDocument and, as a
plan-time snapshot, WorkoutLogDocument — the optional array doubles as the
machine flag. Editable from the exercise editor's new Machine section and
from the workout row's settings sheet, whose mid-workout edits write back
to the originating split (workout saved first, so seed clone-on-edit
repointing can't clobber the log edit).
Schema tightening rides the same rev: splits bump to v2 (weight-reminder
fields and the unused exercise category removed), workouts to v3 (the
derived `completed` flag removed; status is the single source). Starter
seeds regenerated at v2 with unchanged ULIDs; SwiftData cache schema
bumped to rebuild. SCHEMA.md documents the shapes.
Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
Starter splits ship as byte-canonical SplitDocument JSON with fixed
ULIDs (Workouts/Resources/StarterSplits, regenerated by
Scripts/generate_starter_splits.swift) and auto-seed after connect into
a verifiably empty container, re-checked after a settle delay — wrong
guesses are harmless because identical bytes make same-path conflicts
empty and tombstones reap resurrected seeds. Seeds are immutable:
SyncEngine.save(split:) forks an edited seed to a fresh ULID and
soft-deletes the original, whose stub is exempt from pruning
(IndieSync 0.3.0 prune(exempting:)) and vetoes resurrection forever;
split views resolve by id through a redirect map to follow the swap.
Add Starter Splits in Settings restores deleted seeds by lifting the
veto stub and rewriting the bundle bytes.
Also fixes ingestFromWatch bypassing the tombstone veto (a phone-deleted
workout resurrected when a stale watch resent it) and reaps a live file
immediately when its tombstone arrives.
SplitDetailView also picks up the category-grouped exercise sections
from the exercise-category work.
Replaces the app-local copies of the extracted storage core with the
IndieSync 0.1.0 package (pinned from the new tag): ICloudFileManager ->
DocumentFileStore + TombstoneStore, ICloudFileMonitor ->
MetadataObserver (batched events with the content-date churn gate),
and the shared ULID / DocumentCoder / Tombstone / VersionedDocument
pieces. SyncEngine stays app-specific and is rewired onto the package
types; documents rename currentSchema -> currentSchemaVersion to adopt
the package protocol. ULID.make() remains as a shim minting the string
form the app keys on.
Behavior-preserving: identical JSON bytes (same coder config), same
stub wire format (kind encodes as the same strings), same soft-delete
ordering, same 30-day prune. WorkoutDocument keeps its local-calendar
month bucketing rather than adopting TimeBucketedLayout's UTC paths --
changing derivation would strand existing files. The watch target
links the package too (ULID + DocumentCoder via Shared); iOS, watchOS,
and widget targets all build.
Watch sessions now run an HKLiveWorkoutBuilder: heart rate and active
energy are collected live (shown in an in-workout HUD), saved to Health
as a real HKWorkout on completion, and carried back to the phone as
WorkoutMetrics on the workout document. Phone-only workouts get an
estimated Health workout (MET x bodyweight x duration) after a 10s
debounce that a late-arriving watch session cancels; a launch sweep
backfills workouts completed within the last day. Dedupe is keyed on
metrics.healthKitWorkoutUUID plus the metrics source.
Splits gain an activity type (strength, functional, HIIT, core, cardio,
cycling) that categorizes the Health workout and picks the MET value.
A post-workout summary sheet (duration, calories, avg/max HR, HR zones,
total volume) fills in live and is also shown on completed workouts.
Weights can now display in lb or kg (display-only relabel), synced to
the watch over the existing application context.
WorkoutDocument schema 1->2 (metrics are irreplaceable, so older apps
must quarantine rather than strip them); cache schema 2 rebuilds the
SwiftData store with the new metric columns. Deleting a workout in the
app intentionally leaves its Health record in place.
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
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.
- 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