I spent this week ripping out the fixed-routine part of a SwiftUI workout app and replacing it with a small AI planning system.
Not a chatbot bolted onto the side. The app asks for goals, reads a compact set of real constraints, calls Claude through a backend function, then stores the resulting 7-day plan locally in SwiftData.
The old version was simple: two hardcoded push/pull routines and a fixed exercise library. Fine for a prototype, but too rigid. If the user only has dumbbells up to 25kg, trains three days a week, and had a rough sleep week, the app needs to know that before it suggests anything.
The new flow is:
- user configures equipment once
- user enters goals, capped at 300 chars
- app reads a small HealthKit summary if permission exists
- Supabase Edge Function verifies auth and subscription state
- Edge Function calls Claude
- app parses the returned JSON into SwiftData models
- watchOS session tracking uses the generated plan day
The important bit is the prompt shape. I did not want prose back from the model. I wanted app data.
const userMessage = `Goals: ${(goals as string).slice(0, 300)}
Equipment: ${(equipment as string[]).join(", ")}
Available weights (kg): ${(weights as number[]).join(", ")}${healthContext}
Schema: ${PLAN_JSON_SCHEMA}`;
The system prompt is deliberately boring:
Output ONLY valid JSON matching the schema provided.
No explanation. No markdown fences.
Exercise descriptions: 1 sentence max.
Metric units only.
Include warmup and rest seconds.
That boring prompt is the product decision. The UI does not need motivational filler. It needs PlanDay, WarmupExercise, and PlannedExercise rows that can be rendered, edited later, and tracked on the watch.
The response schema is inline and maps directly onto the local model:
{
"planName": "string",
"days": [
{
"dayNumber": 1,
"isRestDay": false,
"warmup": [{ "order": 1, "name": "string", "duration": "string" }],
"exercises": [
{
"order": 1,
"name": "string",
"sets": 3,
"repTargetLow": 8,
"repTargetHigh": 12,
"suggestedWeightKg": 20.0,
"restSeconds": 90
}
]
}
]
}
I also put the API call behind a server-side gateway instead of calling Claude directly from iOS. That gives me three useful controls:
- JWT verification before generation
- subscription check before spending tokens
- monthly usage tracking per user
The current limit is 10 plan generations per month. With the compact prompt, the target budget is roughly 800 input tokens and 600 output tokens per generation. That keeps AI cost predictable instead of letting a mobile text field become an unbounded invoice generator.
On-device still matters here. The generated plan, session logs, equipment inventory, and active plan state live locally in SwiftData. The network is only used to create a new plan. Running a workout should not depend on an API being online.
The lesson: AI features get much easier to ship when you stop treating the model response as content and start treating it as a typed boundary.
Prompt in. JSON out. Validate, store, render, track.
That is less magical than a chat UI, but it is much closer to software.
Source: Recent SwiftUI/watchOS app work: replaced hardcoded routines with a Claude-backed 7-day plan generator, HealthKit summary input, Supabase Edge Function gateway, SwiftData local storage, and monthly usage limits.
Tags: ai, swift, ios, claude
Status: published
Top comments (0)