workout-app

Architecture

High-level shape of the app. Read this when you’re about to make a structural decision; otherwise jump straight to the relevant folder README.

Layers

┌─────────────────────────────────────────────┐
│ Views  (Features/*/<Screen>View.swift)      │  SwiftUI, declarative
├─────────────────────────────────────────────┤
│ View Models  (*ViewModel.swift, @Observable)│  per-feature, when needed
├─────────────────────────────────────────────┤
│ Services  (protocol seams)                  │  FoodResolving, …
├─────────────────────────────────────────────┤
│ Persistence  (SwiftData @Model)             │  PersistenceController owns container
└─────────────────────────────────────────────┘

No tab bar. RootView (in App/) holds a MenuDestination enum and swaps the visible feature screen based on what the user picks in the menu sheet. Each feature can host its own NavigationStack if it needs one — the root stays flat.

Backend wiring

Reference: BACKEND_SETUP.md.

iOS  ──► FoodResolving protocol  ──► (offline) MockFoodResolver
                                  ──► (configured) RemoteFoodResolver
                                            │
                                            ▼
                                   Supabase Edge Function
                                            │
                                            ▼
                                AI Gateway → LLM provider

iOS NEVER holds an LLM API key. The backend Edge Function:

  1. Normalizes the input.
  2. Looks up the canonical food in Postgres (full-text + pgvector).
  3. Calls the AI only on miss.
  4. Validates the AI’s JSON.
  5. Caches the resolved food per 100g/100ml.
  6. Returns the resolved nutrition.

Why SwiftData (not Core Data)

Why no Combine

SwiftUI bindings + @Observable cover the reactive surface. Async work uses Task + async/await. There’s no need for Combine here.

Folder conventions

Don’t