Add the macOS app: a library-management companion (routines, schedules, exercise browser)
A new 'Workouts Mac' target (same bundle ID as iOS, universal purchase) with a NavigationSplitView shell over the shared data/sync layers: routine management with starter gallery and seed-fork follow, schedule editing (reminders stay iPhone-scheduled), and a browse-only exercise library with the animated figures and reference guides. Multi-writer stance: the Mac writes only routine and schedule documents (whole-document last-writer-wins) and never workout documents, whose per-log merge exists only on the watch ingest path. The Mac reconciles on window activation (throttled) since iCloud syncs while the app is closed, and flushes pending writes on deactivation.
This commit is contained in:
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
A workout tracking app for iPhone and Apple Watch, built with Swift 6 and SwiftUI (iOS 26 / watchOS 26). It helps users manage workout splits, track exercise logs, and sync across devices. As of **2.0**, persistence is an **iCloud Drive document architecture**: JSON files in iCloud Drive are the sole source of truth, and a rebuildable SwiftData store is a read-through cache. Core Data, `NSPersistentCloudKitContainer`, CloudKit, and the App Group were all removed in 2.0.
|
||||
A workout tracking app for iPhone, Apple Watch, and Mac, built with Swift 6 and SwiftUI (iOS 26 / watchOS 26 / macOS 26). The Mac app is a library-management companion (routines, schedules, exercise browser — no workout running). It helps users manage workout splits, track exercise logs, and sync across devices. As of **2.0**, persistence is an **iCloud Drive document architecture**: JSON files in iCloud Drive are the sole source of truth, and a rebuildable SwiftData store is a read-through cache. Core Data, `NSPersistentCloudKitContainer`, CloudKit, and the App Group were all removed in 2.0.
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -24,7 +24,7 @@ A workout tracking app for iPhone and Apple Watch, built with Swift 6 and SwiftU
|
||||
### Persistence & Sync (iCloud Drive documents)
|
||||
- **Source of truth**: JSON documents in the iCloud Drive container `iCloud.dev.rzen.indie.Workouts`. One file per aggregate: `Splits/<ULID>.json` and `Workouts/YYYY/MM/<ULID>.json` (month-bucketed; ULIDs sort chronologically).
|
||||
- **SwiftData cache**: `WorkoutsModelContainer` (`Shared/Persistence/`) builds a local SwiftData store as a *rebuildable read-through cache*, created with `cloudKitDatabase: .none`. It is wiped on a schema-version bump or an iCloud account change. Views read it via `@Query`.
|
||||
- **One-way data flow (iPhone)**: view → `SyncEngine.save(...)` writes a file → `MetadataObserver` (an `NSMetadataQuery`) emits a delta batch → `CacheMapper` upserts SwiftData → `@Query` views refresh. The phone is the **sole writer** of iCloud Drive.
|
||||
- **One-way data flow (iPhone & Mac)**: view → `SyncEngine.save(...)` writes a file → `MetadataObserver` (an `NSMetadataQuery`) emits a delta batch → `CacheMapper` upserts SwiftData → `@Query` views refresh. The phone and the Mac both write iCloud Drive; the Mac writes **only routine and schedule documents** (whole-document last-writer-wins — it must never write workout documents, whose per-log merge exists only on the watch ingest path).
|
||||
- **Key types**:
|
||||
- `SyncEngine` (`@Observable @MainActor`, in `Workouts/Sync/`) — the app-specific orchestrator. `connect()` resolves the ubiquity container and reconciles; `save`/`delete` write files; `handle(_:)` applies observer delta batches; `ingestFromWatch(_:)` writes + upserts the cache directly. Exposes `iCloudStatus` and an `onCacheChanged` callback.
|
||||
- The file-side core comes from the **IndieSync** SPM package (`https://git.rzen.dev/rzen/indie-sync.git`): `DocumentFileStore` (actor — all `NSFileCoordinator` I/O, conflict resolution, eviction-safe reads, placeholder-aware enumeration), `TombstoneStore` (soft-delete stubs, resurrection veto, grace-period pruning), `MetadataObserver` (`@MainActor` — wraps the `NSMetadataQuery`, emitting added/modified/removed batches as an `AsyncStream`), plus `ULID`, `DocumentCoder`, `SyncError`, and the `VersionedDocument` protocol. See the `icloud-sync-engine` skill for the architecture invariants.
|
||||
@@ -46,25 +46,27 @@ All in `Shared/Model/`:
|
||||
### App Structure & DI
|
||||
- **iOS**: `WorkoutsApp` (`@main`) owns `AppServices` (`@Observable @MainActor`: `container`, `syncEngine`, `watchBridge`, `workoutLauncher`), injects them via `.environment(...)` / `.modelContainer(...)`, then runs `await services.bootstrap()` (connect + activate). Root is `RootGateView` → `ContentView` → `WorkoutLogsView` (a single screen — there is no TabView).
|
||||
- **watchOS**: `WorkoutsWatchApp` (`@main`) owns `WatchAppServices` (`container` + `bridge`; no iCloud, no SyncEngine) and a `WatchAppDelegate`. Root is `ContentView` → `ActiveWorkoutGateView`.
|
||||
- **macOS**: `WorkoutsMacApp` (`@main`, in `Workouts Mac/`) owns `MacAppServices` (`container` + `syncEngine` only — no watch bridge, HealthKit, Live Activity, or backup). Root is `MacRootGateView` → `MacContentView` (a `NavigationSplitView`: routines sidebar, schedules, exercise browser). Reconciles on window activation (throttled) because iCloud can sync while the app isn't running; flushes pending writes on deactivation.
|
||||
- Views read services with `@Environment(SyncEngine.self)` (etc.), read data with `@Query`, and write with `await sync.save(...)` / `delete(...)`.
|
||||
|
||||
### Watch Sync (WatchConnectivity bridge)
|
||||
- iPhone is the sole writer of iCloud Drive; the watch is a thin remote that round-trips domain documents through the phone, keyed by stable ULIDs.
|
||||
- The watch never touches iCloud Drive; it is a thin remote that round-trips domain documents through the phone, keyed by stable ULIDs.
|
||||
- **Wire format** `Shared/Connectivity/WCPayload.swift`. Phone→Watch: full state (splits + recent workouts) plus settings (rest seconds, done-countdown seconds) via `updateApplicationContext` (latest-wins). Watch→Phone: `workoutUpdate` (one `WorkoutDocument`) and `requestSync`.
|
||||
- **Phone** `Workouts/Connectivity/PhoneConnectivityBridge.swift` — pushes on `onCacheChanged`; inbound updates go to `SyncEngine.ingestFromWatch`.
|
||||
- **Watch** `Workouts Watch App/Connectivity/WatchConnectivityBridge.swift` — a local SwiftData cache fed only by phone pushes; applies updates optimistically, then forwards.
|
||||
- **Watch HealthKit session** (runtime only, *not* sync): `WorkoutSessionManager` holds an `HKWorkoutSession` so the watch stays foregrounded during a workout; the phone launches it via `Workouts/HealthKit/WorkoutLauncher.swift`.
|
||||
|
||||
### Platforms, Tooling & Entitlements
|
||||
- iOS 26 / watchOS 26; Swift 6 with `SWIFT_STRICT_CONCURRENCY: complete` on both targets.
|
||||
- iOS 26 / watchOS 26 / macOS 26; Swift 6 with `SWIFT_STRICT_CONCURRENCY: complete` on all targets.
|
||||
- Build number (git commit count), `BuildDate`, and `BuildHash` are stamped by the `app-versioning` skill's `update_build_info.sh` (referenced in place from `../indie-skills`, a post-build phase; release tagging is opt-in via `RELEASE_TAGGING=1`). `Scripts/` holds the App Store / TestFlight release pipeline.
|
||||
- Entitlements: iOS uses CloudDocuments + HealthKit; watch uses HealthKit only. **No CloudKit service, no App Group.**
|
||||
- Entitlements: iOS uses CloudDocuments + HealthKit; watch uses HealthKit only; macOS uses app sandbox + CloudDocuments (same bundle ID as iOS — universal purchase). **No CloudKit service, no App Group.**
|
||||
- SPM packages: `IndieSync` (file-side iCloud sync core) and `IndieAbout` (in-app About / changelog / license UI).
|
||||
|
||||
### Key Directories
|
||||
- `Shared/` (compiled into both targets): `Model/`, `Persistence/`, `Connectivity/`, `Utils/`, `Screenshots/`
|
||||
- `Workouts/` (iOS): `Sync/` (the persistence/sync layer), `Connectivity/`, `HealthKit/`, `Seed/`, `Views/` (`Common/`, `Exercises/`, `Settings/`, `Splits/`, `WorkoutLogs/`), `Resources/` (Info.plist, entitlements, `StarterSplits/`, `ExerciseMotions/`)
|
||||
- `Workouts Watch App/` (watchOS): `Connectivity/`, `Views/`, plus `WorkoutSessionManager`, `WatchAppDelegate`, `WatchAppServices`
|
||||
- `Workouts Mac/` (macOS): `WorkoutsMacApp`, `MacAppServices`, `Views/` (`Routines/`, `Schedules/`, `Exercises/`), `Resources/`; also compiles `Workouts/Sync/`, `Workouts/Seed/`, `Workouts/ExerciseFigure/`, and a few portable files from `Workouts/Views/` (see `project.yml`)
|
||||
- Root: `project.yml`, `Scripts/`, and `CHANGELOG.md` / `README.md` / `LICENSE.md` (bundled into the iOS app for IndieAbout)
|
||||
|
||||
### Starter Data (deterministic seeds)
|
||||
|
||||
Reference in New Issue
Block a user