Stride
Training plans break the first week you miss a run. I built a running coach that rebuilds the plan around the sessions you skipped instead of leaving you behind it.
iOS · Solo — product, architecture, implementation
- Swift
- SwiftUI
- HealthKit
- CloudKit
- ActivityKit
- WidgetKit
- FoundationModels
- MapKit
The problem worth solving
A training plan is a PDF that stops being true in week two. You miss a Tuesday, then a long run, and now every remaining session is calibrated for a version of you that did the work. Most apps respond by showing you what you failed to do.
Stride treats the plan as something that gets rebuilt, not something you fall behind. That is the product, and it is also the hardest part of the engineering.
Designing for the plan going wrong
Generating a first plan is the easy half — TrainingPlanService builds it from your goal
race, fitness level and available equipment. The interesting half is the set of services
that exist purely for when reality diverges from it:
MissedWorkoutDetectorandMissedWorkoutShift— notice what was skipped and decide what that meansWorkoutReorderPlannerandPlanRebuildWindow— decide what can move, and how far it is safe to move itAdaptableWorkoutSelector— pick the substitute session
Splitting it this way was deliberate. Each piece is a pure decision with an input and an output, which means the plan-repair logic is unit-testable without a device, a simulator, or a fabricated week of HealthKit data. The alternative — repair logic scattered through view models — is the version that quietly stops working and nobody notices, because the failure is a slightly wrong plan rather than a crash.
Coaching that earns the word
The coach layer reads your actual recent training rather than reciting generic advice.
CoachSituation, CoachBaseline and SmartCoachCards assemble what is worth saying right
now and rank it — one primary card carrying a derivation line that shows where its numbers
came from, with supporting cards beneath it.
That derivation line matters more than it looks. Advice a runner cannot trace is advice a runner ignores, and an app that says "take it easy today" without saying why is indistinguishable from an app guessing.
Two policies exist because the standard advice is actively wrong for the people they cover, and I wrote up the evidence behind both in the repo rather than pattern-matching what other apps do:
HeatAdaptationPolicy— how much to pull back when the weather turns, and for how long, paired withHeatAdvisoryServiceand a WeatherKit forecast.PregnancyRunningPolicyandPregnancyPlanService— plan generation for running through pregnancy.
The run itself
Live tracking covers pace, splits and heart-rate zones, with a voice coach announcing splits, lap recording, and pace alerts when you drift off the prescription. A Live Activity puts the run on the Lock Screen and in the Dynamic Island; widgets carry the next session to the Home Screen.
Route Genius generates new routes from wherever you are standing. Its
OpenRouteService token is read from the device Keychain rather than shipped in the bundle —
you paste it once in Settings and it is stored AfterFirstUnlockThisDeviceOnly, never in
UserDefaults and never synced through iCloud. A secret in a plist is the easy version and
the wrong one.
How I know it works
Workouts sync through HealthKit and CloudKit, AppContainer is the composition root for
every repository and service, and the Xcode project is generated from project.yml with
XcodeGen so there is no .pbxproj to conflict on. CI lints with SwiftLint, builds, and
gates on tests; Xcode Cloud handles archive and TestFlight builds, assembling release notes
from commit history.
There is also a UI-test target, UXCaptureTests, that drives the app and captures every
screen with seeded demo data. I have used it to review the app from what actually lands
on the glass rather than from reading source — which surfaces a different class of defect
entirely: text overflowing its frame, controls dimmed with no explanation, two screens
disagreeing about the same number, empty states rendering as a blank box. Several of those
reviews are in the repo, each finding cited to a file:line and tracked to a fix.
What this project demonstrates
Domain modelling where the domain is genuinely hard and the failure mode is subtle rather than loud. Testable architecture chosen for a specific reason. Enough product judgment to know that the feature worth building was not "generate a plan" — every app does that — but "survive the user missing it."