Skip to content

About

Native iOS + Apple Watch workout tracker — paste text plans, get structured sessions with timers, GPS tracking, and HealthKit integration

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

WorkoutTracker

Swift 5.9+ iOS 17+ watchOS 10+

Native iOS + Apple Watch workout tracker that parses LLM-generated text plans into structured sessions with step-by-step timers, GPS tracking, and HealthKit integration.

Why / Tradeoffs / What I'd Change

This project doubles as a testbed for a multi-subagent Claude Code review workflow (see .claude/agents/ — dedicated coder, reviewer, tester, and doc-writer agents). Findings from that process are written up honestly in docs/CODE_REVIEW.md rather than silently fixed and forgotten; a few are still open.

Why the dual-path LLM routing? Plans of roughly ≤3,500 tokens are parsed on-device via Apple Foundation Models (@Generable, iOS 26+) — no network round-trip, no API key. Longer plans, or devices on iOS 17, fall back to Gemini 2.0 Flash Lite via OpenRouter. Tradeoff: two parser implementations that both have to stay compatible with the same ParsedWorkoutPlan output contract, in exchange for not forcing an API key on the common case.

Real tradeoffs the review surfaced (see CODE_REVIEW.md for the full list):

  • The SwiftData ModelContainer fallback path force-unwraps (try!) — if the fallback container itself fails to initialize, the app crashes on launch with no recovery (WorkoutTrackerApp.swift, WatchApp.swift).
  • ExecutionStep.flattenSteps creates Exercise model objects for rest intervals but never inserts them into a ModelContext, leaving orphaned/unmanaged SwiftData instances.
  • LocationService and the Watch WorkoutSessionManager mutate @Observable GPS state directly from CLLocationManagerDelegate callbacks, which fire on an arbitrary thread — a data race, since @Observable isn't thread-safe.
  • The OpenRouter API key ships via Info.plist/xcconfig, which is extractable from the compiled IPA — fine for a personal project, not something to ship at scale.

What I'd change next: WatchConnectivityManager.shared (Shared/Services/WatchConnectivityManager.swift) is still a hard singleton, used directly by SessionExecutionViewModel, WatchApp, and WorkoutSessionManager — CODE_REVIEW.md flags this as blocking unit tests without swizzling (H4/T1). HealthKit and CoreLocation, by contrast, already sit behind HealthKitServiceProtocol/LocationServiceProtocol with service-level tests in Tests/ViewModels/SessionExecutionViewModelTests.swift; extending that same DI pattern to Watch connectivity is the logical next step.

Features

  • Paste & Parse — paste any text workout plan; on-device AI (Apple Intelligence, iOS 18+) or Gemini 2.0 Flash Lite (cloud fallback) structures it into sessions, blocks, and exercises
  • Session Tracker — step-by-step execution with countdown timers, exercise instructions, HR zone display, and RPE
  • GPS Tracking — live pace, distance, and route map (MapKit) for running and cycling blocks
  • Apple Watch — mirrors the active iPhone session with haptics; can also run sessions independently with on-Watch GPS + HR via HealthKit
  • iCloud Sync — plans and history sync across your iPhone and Watch via CloudKit
  • Accessibility — full VoiceOver support across all screens
  • Structured Logging — unified diagnostics via os.Logger throughout the app

Requirements

  • Xcode 15+
  • iOS 17+ (iPhone target)
  • watchOS 10+ (Apple Watch target)
  • Active Apple Developer account (for HealthKit, CloudKit, and Watch)
  • XcodeGen to generate the .xcodeproj

Development

Prerequisites

Setup

# Clone the repo
git clone https://github.com/victorkzam/workout-plan-tracker.git
cd workout-plan-tracker

# Copy secrets template
cp Secrets.xcconfig.template Secrets.xcconfig
# Edit Secrets.xcconfig with your OpenRouter API key

# Generate Xcode project
xcodegen generate

# Open in Xcode
open WorkoutTracker.xcodeproj

Configure your API key (cloud fallback only)

The app uses Apple Foundation Models on-device (iOS 18+) as the primary parser — no API key needed for that path.

For the cloud fallback (iOS 17 or long plans), create Secrets.xcconfig:

cp Secrets.xcconfig.template Secrets.xcconfig

Edit Secrets.xcconfig and set your OpenRouter key:

OPENROUTER_API_KEY = sk-or-v1-YOUR_KEY_HERE

Secrets.xcconfig is gitignored and will never be committed.

Configure signing

Open WorkoutTracker.xcodeproj, select the WorkoutTracker and WorkoutTrackerWatch targets, and set your Team in Signing & Capabilities.

In project.yml, set:

settings:
  base:
    DEVELOPMENT_TEAM: "YOUR_TEAM_ID"

Then re-run xcodegen generate.

Enable CloudKit

In the CloudKit Console, create a container named:

iCloud.com.victorkzam.WorkoutTracker

Running Tests

xcodegen generate
xcodebuild test -scheme WorkoutTrackerTests -destination 'platform=iOS Simulator,name=iPhone 16'

Linting

swiftlint lint

Run

Build and run the WorkoutTracker scheme on your iPhone. The Watch app is automatically pushed if a paired Watch is connected.

Architecture

workout-plan-tracker/
├── Shared/                    # Shared between iOS + Watch (SwiftData models, WatchConnectivity)
│   ├── Models/                # WorkoutPlan, WorkoutSession, WorkoutBlock, Exercise, SessionExecution
│   ├── Protocols/             # Service protocol abstractions
│   ├── Services/              # WatchConnectivityManager
│   └── Utilities/             # Shared utilities (GPSMath, Logging, ModelContainerFactory)
├── iOS/
│   ├── App/                   # App entry point, Info.plist, entitlements
│   ├── Services/              # WorkoutParserService, AppleFoundationParser, OpenRouterParser,
│   │                          # HealthKitService, LocationService
│   ├── ViewModels/            # PlanImportViewModel, SessionExecutionViewModel
│   └── Views/
│       ├── Plans/             # PlanListView, PasteImportView, PlanDetailView, SessionDetailView
│       ├── Sessions/          # SessionExecutionView, ExerciseStepView, GPSRunView
│       └── Components/        # HRZoneTag, PaceDisplay, BlockTypeBadge
├── Watch/
│   ├── WatchApp.swift         # Entry point + WatchSessionController
│   ├── Services/              # WorkoutSessionManager (HKWorkoutSession + GPS)
│   └── Views/                 # SessionListWatchView, SessionWatchView, GPSWatchView, ...
├── Tests/                     # Unit tests with mocks
├── .claude/agents/            # Claude Code subagent definitions
└── docs/                      # Project documentation

LLM Parsing

Path When used Model
On-device iOS 26+, plan ≤ 3 500 tokens Apple Foundation Models (@Generable)
Cloud (OpenRouter) iOS 17, or plan > 3 500 tokens Gemini 2.0 Flash Lite

Both paths produce the same ParsedWorkoutPlan struct, which is converted to SwiftData models.

Documentation

Verification checklist

  • Paste the sample half-marathon plan → 4 sessions created, Session 1 has 5 blocks
  • Core circuit block: rounds=2, workIntervalSec=45, restIntervalSec=15
  • Run block has exerciseType=gpsRun, pace and HR zone populated
  • Start Session 1 → warm-up steps auto-advance every 30 s
  • Core circuit repeats × 2 rounds with 15 s rest steps between exercises
  • GPS run screen shows live pace + route polyline
  • Start session on iPhone → Watch mirrors current step within 3 s
  • Start GPS run from Watch → HKWorkoutSession records distance + HR independently
  • Plan added on iPhone → appears on Watch after CloudKit sync

About

Native iOS + Apple Watch workout tracker — paste text plans, get structured sessions with timers, GPS tracking, and HealthKit integration

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages