Read this first, then go straight to the folder that owns the change. Don’t grep the whole repo. Each folder has a short README with file index + extension points.
A native iOS 26 SwiftUI nutrition + workout journal, “Notes-app simple.” iOS-only. Local persistence via SwiftData; cloud sync via Supabase (per-user, RLS-scoped). Full product spec lives in ai_build_reference_health_workout_app.md.
Status: Phase 1.5 — UI is done in its current shape; backend schema is committed but not yet applied; iOS still runs MockFoodResolver. Next iterations wire Supabase Edge Function + Sign in with Apple, then cardio + progression engine.
App/WorkoutAppApp.swift launches JournalView directly inside a NavigationStack. There are no tabs and no custom floating bar — Apple-native primitives only.
┌─────────────────────────────────────────────┐
│ Monday, April 27 [👤] │ ← custom header HStack (nav bar hidden)
│ Chicken 150 C │
│ Workout 0 kg │
│ Weight 96 kg │
│ 2 cups of water 0.5 L │
│ Write a food... │ ← rotating placeholder
│ │
│ ┌─ SummaryCard ────────────────────────┐ │
│ │ ⭕20% 450/2226 15g 54g 18g 💧 │ │ ← .safeAreaInset(.bottom)
│ │ 1776 left p c f 0.2L │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
NavigationStack toolbar is hidden via .toolbar(.hidden, for: .navigationBar)) — plain bold date on the left + person.crop.circle.fill profile button on the right. Rolled by hand because iOS 26 wraps every ToolbarItem(.button) in a Liquid-Glass capsule, which clipped the date to “Tu…”. Profile button → ProfileView sheet (large detent).SummaryCard at the bottom — single Mist-style row, monochrome (only the cup is blue). Tap → InsightsView sheet (large detent).ActiveWorkoutCard takes the same safeAreaInset slot when a workout session is active.Write a food... / Write a workout... / Write your weight... / Write a drink... / Write a run... / Write a swim...), picked deterministically from the day-of-era so it advances once at midnight.Everything the user can log gets typed into the same field. JournalViewModel.submit tries intercepts in this order:
workout / gym / lift / train, optional name hint after a space) → start/end an ActiveSession.run / running / jog / endurance / sprint / swim / row / bike / cycle / cardio, optional context after a space) → start a CardioBout and mount ActiveCardioCard in the same overlay slot the workout card uses.96 kg, 185 lb, 70.5 kilo, 96kg) → WeightLogEntry.FoodResolving resolver → FoodLogEntry.Four log types share the day’s feed (FeedItem.food / .workout / .weight / .cardio), sorted chronologically with swipe-to-delete and per-type detail sheets.
Theme.Colors.water). No purple, no other accent. Macro state colors (red/green) were tried and removed..spring(response: 0.45, dampingFraction: 0.82))..cardBackground() (rounded white) for inside content; the bottom summary uses a hand-rolled rounded-rect with Theme.Shadow.card.WorkoutApp/
├── App/
│ ├── WorkoutAppApp.swift ← @main entry, hosts JournalView
│ ├── AppEnvironment.swift ← DI container (resolver picker)
│ ├── AppConfig.swift ← reads Info.plist → Supabase + AI keys
│ └── SummaryCard.swift ← Mist-style bottom summary
├── Design/ ← Theme + view modifiers + atoms (CalorieRing, WaterCup, …)
├── Models/ ← @Model SwiftData types
├── Services/ ← FoodResolver protocol + impls + PersistenceController
├── Features/
│ ├── Journal/ ← JournalView, ActiveWorkoutCard, food/weight/workout rows
│ ├── Insights/ ← (legacy Overview/Nutrients/Training — slated for redesign)
│ └── Profile/ ← Form-based settings + detail pages
└── Resources/ ← Assets + Info.plist (managed by xcodegen) + Secrets.example.xcconfig
supabase/
├── migrations/0001_init.sql ← schema + RLS + auth trigger
└── functions/resolve-food/ ← edge fn (next iteration)
docs/
├── BACKEND_SETUP.md ← step-by-step infra setup
├── ARCHITECTURE.md ← bigger-picture
├── BUILD_AND_RUN.md
├── DESIGN_SYSTEM.md
└── ROADMAP.md
scripts/ ← seed-usda, seed-open-food-facts (next iteration)
xcodegen generate rebuilds WorkoutApp.xcodeproj from project.yml. The managed Info.plist injects SUPABASE_URL / SUPABASE_ANON_KEY / AI_PROVIDER / AI_API_KEY from the gitignored Secrets.xcconfig via $(VAR) substitution at build time.
Journal: user types "gym" + Done
→ JournalViewModel.submit detects keyword → sets pendingWorkoutLaunch
→ JournalView.handleWorkoutToggle creates WorkoutSession(status: .active)
→ ActiveWorkoutCard slides up via .safeAreaInset(.bottom), replaces SummaryCard
→ Templates dropdown auto-opens with "New blank workout" + saved templates
User picks a template OR "New blank workout" → exercise 1 created with EMPTY name
→ user taps to type the exercise name
User types reps/weight. Swipe left/right anywhere on the card body → next /
previous exercise. The `+` next to the exercise name appends a fresh blank
exercise. Subtle chevron-in-circle hints on the left and right edges of the
card make the swipe affordance discoverable.
User taps anywhere outside the card (on the dimmed background) → marks the
session completed → card slides out → row appears in the day's feed as
`Workout` on the left + total kg/lb pushed on the right (no subtitle —
each journal row carries a single trailing value). Tapping a completed
row re-mounts the same `ActiveWorkoutCard` in the centered overlay
(populated with the session's exercises/sets), *not* a sheet — so the
review feels like a fresh log card with the data filled in.
Finish: tap anywhere on the dimmed area outside the card → marks the
session completed and saves whatever's been entered. To delete a
finished session entirely, swipe-left the journal row.
Workout plans live in Profile → Workout plans. Each plan has a name + JSON-encoded [TemplateExercise] (each with default [TemplateSet] carrying optional reps/weight). They surface in the active card’s chevron-down dropdown.
Read docs/BACKEND_SETUP.md for the full setup. Headlines:
auth.uid()).resolve-food) so the AI key stays server-side; iOS never sees it.SUPABASE_URL + SUPABASE_ANON_KEY (designed to be embedded). Master DB password and Groq key live only in supabase secrets and scripts/.env.USER TYPES "150g chicken" (or "1 cup broccoli")
│
iOS parses quantity + name
│
▼
Edge Function `resolve-food`:
1. Search foods + food_aliases
HIT → scale per-100g macros to 150g (or look up `cup → grams` via
food_servings, then scale)
MISS → call Groq with the resolver prompt
↓
Validate JSON → INSERT foods row + aliases + servings
↓
Return resolved entry
│
▼
iOS writes food_log_entries row (RLS scopes to user)
When no quantity is given ("chicken"), the resolver assumes 100g. Foods include food_servings rows so volume inputs ("1 cup broccoli") resolve via the food + cup-to-grams lookup. Failed AI / offline → entry stored as needs_review; tapping the row opens an editable detail sheet for manual macro entry.
Write-only. We push food / water / workout / weight logs to HealthKit so the user’s other apps see them. We never read from HealthKit. NSHealthShareUsageDescription will be removed when the HealthKit writer ships.
Algorithm shape (rules-based, no AI). Every exercise the user logs in a workout_sessions accumulates into per-exercise history. When the user opens a new session for the same exercise, the resolver suggests the next set:
This lives in Services/Progression/ (file does not exist yet). Computed on session open from the previous N sessions; user can override anytime.
Cardio is a different shape from strength — distance + time + effort instead of reps × weight. Lives in:
Models/Cardio.swift — CardioKind, DistanceUnit, @Model CardioBout (with intervalsEnabled toggle + @Relationship to CardioIntervals).Features/Journal/JournalViewModel.swift — CardioLaunchIntent.parse matches run / running / jog / jogging / endurance / sprint(s) / sprinting / swim / swimming / row / rowing / bike / biking / cycle / cycling / cardio. Sets pendingCardioLaunch.Features/Journal/ActiveCardioCard.swift — kind dropdown (Run ⌄) on the left, X discard on the right; per-interval row with distance field + unit menu (m/km/yd/mi), duration field (mm:ss / hh:mm:ss), free-text effort (max / 60% / easy), + Add interval (only when toggle on), Intervals toggle, blue ✓ to finish.Features/Journal/CardioSummaryRow.swift — Mist-style row, kind name + total distance in km (kg user) or mi (lb user).Features/Journal/JournalView.swift — displayedCardioBout shares the centered overlay slot with displayedWorkoutSession (mutually exclusive). Both support tap-outside-to-finish (saves whatever’s been typed and marks completed). Cardio additionally has a one-tap X discard in the header.Inline shorthand parsing ("5 km swim", "30 min bike", "5x100m sprints") is not yet wired — typing a cardio keyword always opens the card; the user fills the fields.
Three sections (replaces the legacy Overview/Nutrients/Training tabs):
Apple-Health-style cards. Designed; not implemented.
| If you want to… | Open |
|---|---|
| Change visual tokens (colors, spacing, shadows) | WorkoutApp/Design/Theme.swift — never hardcode |
| Change Journal layout / behavior | WorkoutApp/Features/Journal/README.md |
| Change the workout card / template dropdown | WorkoutApp/Features/Journal/ActiveWorkoutCard.swift |
| Change the bottom summary card | WorkoutApp/App/SummaryCard.swift |
| Change the calorie ring or water cup | WorkoutApp/Design/CalorieRing.swift + WorkoutApp/Design/WaterCup.swift |
| Change input grammar / placeholder rotation | WorkoutApp/Features/Journal/JournalViewModel.swift + FoodInputField.swift |
| Change Profile fields, units, plans | WorkoutApp/Features/Profile/README.md |
| Edit a workout plan UI | WorkoutApp/Features/Profile/WorkoutPlanEditor.swift |
| Change food → nutrition resolution | WorkoutApp/Services/FoodResolver/README.md |
| Wire / change the backend | docs/BACKEND_SETUP.md + supabase/migrations/ + WorkoutApp/Resources/Secrets.example.xcconfig |
| Add a SwiftData field/entity | WorkoutApp/Models/README.md |
| Build / run | docs/BUILD_AND_RUN.md |
| Bigger architectural picture | docs/ARCHITECTURE.md |
| What’s next | docs/ROADMAP.md |
Design/Theme.swift. Never hardcode.NavigationStack, Form, .toolbar, .sheet, .safeAreaInset are the building blocks.Services/.xcodegen generate after adding a file or folder. Sources are auto-discovered, but the project file needs a refresh.SUPABASE_URL + SUPABASE_ANON_KEY.@Model fields requires no migration; adding non-optional fields breaks dev stores. New fields → optional + accessor with fallback.WorkoutTemplate.templateExercises is the canonical source of plan content (JSON-backed). Legacy exerciseIds is only for back-compat reads.xcodegen (Homebrew) — generates .xcodeproj from project.yml.xcodebuild — verifies the app builds for an iPhone simulator (iPhone 17 is the default target).supabase CLI (Homebrew) — applies migrations + deploys edge functions.