Workouts 2.0: re-base persistence on iCloud Drive documents

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.
This commit is contained in:
2026-06-19 14:25:27 -04:00
parent 9a881e841b
commit 85d0eaddbb
86 changed files with 3873 additions and 3755 deletions
+58 -173
View File
@@ -1,187 +1,72 @@
# Workouts App Requirements
## Overview
A workout tracking iOS application with Apple Watch companion that helps users manage workout splits, track exercise logs, and sync data across devices using CloudKit.
A workout tracking iOS app with an Apple Watch companion that helps users manage
workout splits, run sessions, and track progress. Data is stored as JSON
documents in the user's iCloud Drive and synced across devices.
## Platform Requirements
- iOS app (iPhone)
- watchOS app (Apple Watch companion)
- CloudKit sync between devices
- SwiftData for persistence with CloudKit automatic sync
- iOS app (iPhone) — iOS 26+
- watchOS app (Apple Watch companion) — watchOS 26+
- Swift 6, strict concurrency; SwiftUI throughout
- Project generated with XcodeGen (`project.yml`)
## Core Data Models
## Persistence Architecture
- **iCloud Drive JSON documents are the sole source of truth.** One JSON file per
aggregate root under the app's iCloud container `Documents/`:
- `Splits/<ULID>.json` — a `SplitDocument` embedding its `[ExerciseDocument]`
- `Workouts/YYYY/MM/<ULID>.json` — a `WorkoutDocument` embedding its `[WorkoutLogDocument]`
- `Stubs/<ULID>.json` — soft-delete tombstones (30-day GC)
- **SwiftData is a rebuildable read-through cache.** App code never writes the
cache directly: every save/delete writes a file; an `NSMetadataQuery` observer
(and a connect-time reconcile) is the sole cache mutator. The cache is wiped and
rebuilt on schema-version bump or iCloud-account change.
- **No CloudKit.** Container: `iCloud.dev.rzen.indie.Workouts` (CloudDocuments).
- **iCloud is required** — without it the app shows a blocking "iCloud Required"
gate (no local-only fallback).
### Split
- `name: String` - Name of the workout split
- `color: String` - Color theme for the split
- `systemImage: String` - SF Symbol icon
- `order: Int` - Display order
- Relationship: One-to-many with Exercise
## Data Model
- **Split**: `id` (ULID), `name`, `color`, `systemImage`, `order`, timestamps;
embeds Exercises.
- **Exercise**: `id`, `name`, `order`, `sets`, `reps`, `weight`, `loadType`
(none/weight/duration), `durationTotalSeconds`, `weightLastUpdated`,
`weightReminderTimeIntervalWeeks`.
- **Workout**: `id` (ULID), `splitID`/`splitName` (denormalized), `start`, `end?`,
`status` (notStarted/inProgress/completed/skipped), timestamps; embeds logs.
- **WorkoutLog**: `id`, `exerciseName`, `order`, `sets`, `reps`, `weight`,
`loadType`, `durationTotalSeconds`, `currentStateIndex`, `completed`, `status`,
`notes?`, `date`.
### Exercise
- `name: String` - Exercise name
- `order: Int` - Order within split
- `sets: Int` - Number of sets
- `reps: Int` - Number of repetitions
- `weight: Int` - Weight amount
- `loadType: Int` - Type of load (weight units)
- Relationship: Many-to-one with Split
Identity is a ULID string (stable across cache rebuilds). Duration is stored as
integer seconds.
### Workout
- `start: Date` - Workout start time
- `end: Date?` - Workout end time (optional)
- `status: WorkoutStatus` - Enum (notStarted, inProgress, completed, skipped)
- Relationship: Many-to-one with Split (optional)
- Relationship: One-to-many with WorkoutLog
## Apple Watch
The phone is the only device that touches iCloud Drive. The watch keeps its own
local SwiftData cache fed only by WatchConnectivity:
- Phone → Watch: pushes all splits + recent workouts (application context) on
every cache change.
- Watch → Phone: sends an updated `WorkoutDocument` (keyed by ULID); the phone
applies it through the file write path (the sole writer of iCloud Drive).
### WorkoutLog
- `exerciseName: String` - Name of the exercise
- `date: Date` - When performed
- `order: Int` - Order in workout
- `sets: Int` - Number of sets completed
- `reps: Int` - Number of reps completed
- `weight: Int` - Weight used
- `status: WorkoutStatus?` - Completion status
- `completed: Bool` - Whether exercise was completed
- Relationship: Many-to-one with Workout
## Seed Data
Starter splits (Upper Body / Core / Lower Body) are generated on demand from the
bundled YAML exercise catalogs (`Workouts/Resources/*.exercises.yaml`) — never
auto-seeded. The catalogs also back the in-workout exercise picker.
## iOS App Features
### Main Navigation (TabView)
1. **Workouts Tab**
- List all workouts sorted by start date (newest first)
- Create new workout from split
- Edit/delete existing workouts
- Navigate to workout logs
2. **Logs Tab**
- Historical exercise completion records
- Grouped by workout
3. **Reports Tab**
- Workout statistics and progress tracking
4. **Achievements Tab**
- User achievements and milestones
### Split Management
- Create/edit/delete workout splits
- Assign colors and system images
- Reorder splits
- Add/remove/reorder exercises within splits
### Exercise Management
- Add exercises to splits
- Set default sets, reps, weight
- Reorderable list within each split
- Support for different load types
### Workout Flow
- Start workout from selected split
- Auto-populate exercises from split template
- Track exercise completion status
- Update sets/reps/weight during workout
- Mark exercises as completed/skipped
- Auto-prevent device sleep during workouts
## Watch App Features
### Main View
- List active workouts
- Show "No Workouts" state when empty
- Sync button to pull from CloudKit
- Display sync status and last sync time
### Workout Display
- Show workout name and status
- List exercises with completion indicators
- Quick completion toggles
### CloudKit Sync
- Manual sync trigger
- Hybrid approach: SwiftData for local storage + direct CloudKit API for sync
- Sync from `com.apple.coredata.cloudkit.zone`
- Fetch and save Splits, Exercises, Workouts, and WorkoutLogs
## Data Management
### Exercise Library
- YAML-based exercise definitions in `Resources/` directory
- `ExerciseListLoader` to parse YAML files
- Preset libraries:
- Bodyweight exercises (`bodyweight-exercises.yaml`)
- Planet Fitness starter (`pf-starter-exercises.yaml`)
### CloudKit Configuration
- Container ID: `iCloud.com.dev.rzen.indie.WorkoutsV2`
- Automatic sync via `ModelConfiguration(cloudKitDatabase: .automatic)`
- Development environment for debug builds
- Production environment for release/TestFlight builds
## UI/UX Requirements
### Design Patterns
- SwiftUI throughout
- NavigationStack for hierarchical navigation
- Form-based editing interfaces
- Sheet presentations for modal operations
- Swipe gestures for edit/delete/complete actions
### Visual Design
- Custom color system via Color extensions
- Consistent use of SF Symbols
- Reorderable lists using SwiftUIReorderableForEach
- Calendar-style list items for workouts
- Checkbox components for completion tracking
### Key UI Components
- `CalendarListItem` - Formatted date/time display
- `Checkbox` - Visual completion indicators
- `SplitListItemView` - Split display with icon and color
- `ExerciseListItemView` - Exercise display with sets/reps/weight
- `WorkoutLogListItemView` - Log entry display
## Technical Architecture
### Project Structure
```
Workouts/
├── Models/ # SwiftData models
├── Schema/ # Database schema and migrations
├── Views/ # UI components by feature
│ ├── Splits/
│ ├── Exercises/
│ ├── Workouts/
│ └── WorkoutLog/
├── Utils/ # Shared utilities
└── Resources/ # YAML exercise definitions
Workouts Watch App/
├── Schema/ # Watch-specific container
├── Views/ # Watch UI components
└── CloudKitSyncManager.swift # Direct CloudKit sync
```
### Key Technical Decisions
- SwiftData with CloudKit automatic sync
- Versioned schema with migration support
- Single file per model convention
- No circular relationships in data model
- Platform-specific implementations for iOS/watchOS
- Shared model definitions between platforms
## Features
- Create/edit/delete/reorder workout splits with custom colors and SF Symbol icons.
- Add exercises to splits (from the bundled catalog or manually); set default
sets/reps/weight or a duration.
- Start a workout from a split; track per-exercise set/rest/done progress; mark
complete/skipped; edit the plan during a session (mirrored back to the split).
- Weight-progression charts per exercise across past sessions.
- Apple Watch: run workouts from the wrist; changes sync back to the phone.
## Dependencies
- **Yams**: YAML parsing for exercise definitions
- **SwiftUIReorderableForEach**: Drag-to-reorder lists
- **Yams** YAML parsing for the exercise catalogs.
- **IndieAbout** — About section in Settings (bundles LICENSE.md / CHANGELOG.md).
## Entitlements Required
- CloudKit
- Push Notifications (aps-environment)
- App Sandbox
- File access (read-only for user-selected files)
## Build Configuration
- Swift Package Manager for dependencies
- Separate targets for iOS and watchOS
- Shared CloudKit container between apps
- Automatic provisioning and code signing
## Release
- TestFlight / App Store via `Scripts/release.sh` (xcodebuild archive +
App Store Connect API upload; credentials in `.env.release`). The iOS archive
embeds the watch app.