Skip to content

Vital

A wellness app that admits what it doesn’t know.

Vital is my independent iOS app for reading health signals day to day. I designed it and built it in SwiftUI, with Claude and Codex working from one shared set of project rules.

It brings together Apple Health data, things a person logs themselves, calculated wellness scores and an AI coach. Each of those means something different, and the product only works if it never presents one as another.

  • iOS product design
  • SwiftUI design system
  • Interaction design
  • HealthKit
  • AI interaction
  • Claude / Codex
My role
Product design, interaction prototyping and the design system
Platform
iOS app with watchOS and widgets, built in SwiftUI
Data
Apple Health, SwiftData with private CloudKit sync
Status
Private beta, in active development
Soft meadow landscape artwork used as Vital’s brand background.
Vital brand artwork / Marketing imagery, not a data visualization

01The problem

A full dashboard can still be wrong.

A wellness dashboard looks finished when every card has a number. The trouble is that some of those numbers aren’t known.

Vital combines four kinds of input: measurements from Apple Health, things the person logs themselves, scores the app calculates, and the things it simply doesn’t have yet. A heart-rate reading is an observation. A readiness score is an estimate built on top of observations. A missing night of sleep data is neither, and it is not a zero.

If the interface treats those the same, it starts telling people things that aren’t true: a low score that is really missing data, or a confident AI summary built on an empty day. The design problem was to keep the daily picture useful without making it look more certain than its inputs allow.

The rule I kept coming back to: if Vital doesn’t know something, it should say so, in the same place the number would have been.

02How it was made

One designer, one codebase, several AI tools.

Vital is an independent product. There’s no client team; I use AI-assisted prototypes to explore design decisions and review the interaction.

What I did

  • Product and interaction design: information model, flows, states and copy.
  • The native design system: tokens, type ramp, MasterCard variants and recipes.
  • SwiftUI implementation, data architecture decisions and the audit work described below.
  • The rules and documentation that keep Claude, Codex and other tools consistent.

How the AI tools stay consistent

  • One canonical AGENTS.md is read by every AI coding tool on the repo; CLAUDE.md is a link to it, so there is a single set of rules.
  • docs/INDEX.md is the starting point, then DESIGN_SYSTEM, FEATURE_PLAN, sync and audit documents for the area being changed.
  • Fixed bugs are logged as numbered issues, so the next session inherits the reasoning, not just the code.
  • The documentation is honest about its own gaps: the repo’s knowledge graph is marked as stale, and current source wins.

No user interviews, clinical review or team process are implied. Where this page describes a decision, it’s a design rationale from the source and docs.

03Four kinds of information

Observed, entered, estimated, or unavailable.

The core of the information model: every value on screen belongs to exactly one of these, and the interface shows which.

  1. 01Observed

    Read from Apple Health, such as heart rate, HRV, sleep stages or workouts.

    Shown with its date and source. Never adjusted to look complete.

  2. 02Entered

    Logged by the person: habits, journal entries, meals, fasting.

    Shown as theirs and editable. An AI meal estimate needs review before it’s saved.

  3. 03Estimated

    Calculated by the app’s scoring layer, such as readiness, strain or stress.

    Optional by design. If the inputs aren’t there, there’s no score.

  4. 04Unavailable

    Not shared, not synced yet, or not recorded for this day.

    An explicit “—” or a no-data card with a reason and, where it exists, a real retry.

Information model drawn for this case study from the scoring, persistence and audit sources. It’s an explanatory diagram, not a project artifact.

04Consent to action

Ask first. Then show only what’s there.

Consent comes first, and every later step can end in “not enough data” without breaking.

Person / From consent to a reviewed action

  1. 01Health AccessChoose what Apple Health may share, in the native sheet
  2. 02Starting pointOnboarding waits for real data to settle, then calculates
  3. 03TodayObservations and optional scores for the selected day
  4. 04Ask or planAsk the coach, or draft an activity
  5. 05ReviewCheck an estimate or plan before it becomes a record
  • Access limitedAffected cards say what’s unavailable and how to share it; the rest of the app still works.
  • Still syncingAfter 20 seconds onboarding continues with “Still syncing in the background” instead of faking progress.
  • No data for the dayScores show “—” and the AI summary is told which values are missing, so it doesn’t advise on them.

Flow reconstructed for this case from the onboarding, scoring and summary sources. It explains the behavior; it isn’t an original flow file.

05Real screens

The empty states are the product.

Real screens from the app, most of them captured in the iOS Simulator with no Apple Health history: exactly when honest states matter.

Vital welcome screen with the Vital mark, the line “Every Step Counts”, and Continue with Apple and Continue with Google buttons.
Simulator / Welcome and sign-in, set in Chillax
The native Health Access sheet asking which Apple Health categories Vital may read.
App capture / Native Health Access, before any data
Vital Today screen with Sleep, Readiness and Steps rings showing a dash, Strain and Stress cards marked No data, empty vitals and an Input today’s workout prompt.
Simulator / Today with no data: every score shows “—”
Vital Trends screen: the Vital Score card shows a grey placeholder curve and the message that no score is recorded for the month yet, above a locked Premium card.
Simulator / Trends: the no-data placeholder, then a Premium lock
Vital Fitness screen: Today Strain shows a dash with a No data pill and a note that strain appears once activity syncs; Activity Builder offers Build a session and Quick start.
Simulator / Fitness: “No data” plus two clear ways to start
Vital AI coach entry screen with a greeting and suggested starter questions, and no personal readings.
App capture / Coach entry with starter prompts and no history
Activity Builder showing an Upper Body Push draft with editable exercises, sets and repetitions.
App capture / A workout draft, not a recorded session

Simulator captures from a Debug build (iPhone 18 Pro) using the project’s documented simulator sign-in bypass, with no HealthKit data. The other three are earlier reviewed app captures. Empty and permission states are shown on purpose; no health readings are presented as results.

06Removing invented data

Every number had to prove where it came from.

The most important design work on Vital was taking numbers away.

An audit of every shipping screen traced each displayed value back to its source. A number of surfaces were showing values that looked like the person’s data but weren’t: preview defaults, card-local formulas and timers that only looked like progress. Each one was replaced with a real source, or with an honest empty state.

Surfaces changed by the mock-data audit
SurfaceBeforeAfter
Biological age cardA preview default (39.7, “5.5 years older”) appeared as the person’s result.Reads the real aging assessment, or shows nothing to report.
Daily Readiness & SleepCard-level formulas such as sleep ÷ 8 h disagreed with Today.One scoring layer for every surface; “--” when there’s no score.
OnboardingFixed 2 s and 6.2 s timers ticked checkmarks unrelated to any work.Waits for real ingestion to settle and recalculates; times out honestly.
Daily JournalA fixed 1/4 completion ring.Counts the person’s active habits; with none, invites them to add one.
Steps distanceEstimated as steps × 0.000762.Apple Health walking distance, or “—”.
AI daily summaryMissing recovery, sleep and strain were passed in as real zeros.Missing values are marked unavailable, and the summary only uses scores that exist.

Before and after examples taken from the project’s mock-data audit. Debug-only demo tools remain, but they’re excluded from Release builds.

07The design system

Few choices, used everywhere.

Vital’s design system lives in code. The documentation is written so a person or an AI tool composes from it instead of inventing.

Type ramp
7 sizes only: 10 · 12 · 14 · 16 · 22 · 34 · 56 pt
Weights
Regular and Medium. There is no bold in the system.
Families
Chillax by default, Space Grotesk as an alternative
Radii
Card 32 → inner card 20 → chip 12. Three steps, never four.
Spacing
12 pt between cards, 4 pt inside them
Targets
44 pt minimum for anything tappable
Motion
expand (spring), chartDraw (1.1 s), tintShift (0.25 s, color only)
Themes
System, Light, Dark and Contrast
Radius nesting: card 32, inner card 20, chip 12. Variants: standard, hero, inner, tiled, prominent.

56

Today

Sleep last night

Your curve draws as soon as samples sync.

SOURCE · APPLE HEALTH

10 · 12 · 14 · 16 · 22 · 34 · 56 pt

Specimen set in Space Grotesk, Vital’s alternative family; Chillax, the default, is visible in the app captures. Sizes follow the documented ramp.

Before writing a new view, the docs ask for a 60-second reuse audit: does a component exist, is there a card to borrow from, is the pattern documented, is there a token for this number, and is this the third copy? If so, extract it.

08Components & states

Recipes for saying “not yet”.

Three recipes from the design-system docs carry most of the “honest state” work.

No data yet

GhostDataPlaceholder keeps the chart card’s shape: a dashed baseline and a monochrome placeholder curve, a short explanation, and “Check again” only where a real refresh exists.

Reduce Motion stops the draw-in and sheen.

Status pill

Optimal, Watch and Needs attention come from one enum that provides both the tint and the label, so status never depends on color alone.

Hand-rolled colored capsules aren’t allowed.

One dominant action

An entry card shows exactly one filled primary action and one quieter secondary, and they change with state: “Build a workout” / “Quick start” becomes “Start workout” / “Save”.

Emphasis comes from fill and weight, never from hue.

Illustrative specimens of the documented recipes, drawn for this page; not the SwiftUI components themselves.

09The AI boundary

Two routes, described accurately.

“Private AI” is easy to promise. Vital has two AI routes, and the interface needs to be accurate about both.

  1. Coach chatCloud: Gemini

    Requests go through a fixed set of validated, read-only tools that return aggregates with bounded inputs, not the whole health record.

  2. Daily summaryOn device first

    Uses Apple’s on-device model where it’s available, with a fallback. Missing values are labelled as missing in the prompt.

  3. Meal photoCloud: Gemini, on request

    Only runs when the person taps Estimate; the result is a draft to review before saving.

So the copy never says all AI stays on the device. Chat sends the requested aggregates to a cloud model, and guidance is wellness information, not medical advice.

10Design decisions

Trade-offs I’d make again.

Each of these is a trade-off I chose on purpose.

D1

Optional scores, not zeros

A missing score stays empty instead of defaulting to 0. It costs more empty-state design, but a low result and an absent one can never be confused.

D2

Monochrome cards, color for meaning

Card surfaces stay neutral, so color only appears where it says something about a metric. It’s quieter, and status still carries a text label.

D3

A deliberately small type system

Seven sizes and two weights. It limits expression, but it makes every screen, including AI-written ones, land on the same hierarchy.

D4

Plans aren’t achievements

A workout draft is editable prescription until a real session is recorded with live heart rate and energy. Manual save is a fallback, not the default.

D5

Honest waiting over fake progress

Onboarding takes as long as the data does, up to a timeout, rather than playing a timed animation that suggests work is happening.

11Accessibility

Built into the tokens, with one gap named.

WCAG 2.2 AA is the reference for contrast and non-color cues, alongside Apple’s Human Interface Guidelines for native behavior; APCA is a supplementary readability check.

Touch targets
A 44 pt minimum for interactive elements, set as a design-system token.
Status without color
Status pills always pair a tint with a word, and unavailable values use a visible “—” rather than a faded color.
Reduce Motion
Chart draw-in and the placeholder sheen respect the Reduce Motion setting; the color-only tint shift doesn’t move anything.
VoiceOver labels
Custom controls carry accessibility labels across the shared components and feature screens.
Readable hierarchy
The fixed type ramp and medium weight keep values, units and explanations distinct at a glance.
What’s still open
Dynamic Type support is limited today and is the most important accessibility gap to close.

Based on the source and design-system rules. No VoiceOver audit, user testing or formal conformance review is claimed.

12Handoff & maintenance

A brief any tool can follow.

Here the “handoff” is between sessions and tools. The repo is set up so any tool picks up the same rules.

Change brief / Format used with Claude and Codex on the Vital repo
Start
Read AGENTS.md, then docs/INDEX.md and DESIGN_SYSTEM for the area.
Compose
Use MasterCard and its variant, the type-ramp role and existing recipes.
Provenance
Say whether each value is observed, entered or estimated, and what shows when it’s missing.
States
Empty, no permission, syncing, error and Reduce Motion.
Don’t
Hard-code a number that has a token, or ship a preview value outside Debug.
Record
Log the fix as the next numbered issue and update the docs you changed.

The data layer keeps the same promise: records the person creates sync through private CloudKit, while the bookkeeping for reading Apple Health stays local to each device. One scoring layer publishes optional values that every surface reads.

13Scope & boundaries

What exists, and what I’m not claiming.

  • A native SwiftUI app in private beta, with watchOS and widget companions built on the same scoring.
  • A documented design system with a fixed type ramp, radii, spacing, motion and themes, and reusable card variants and recipes.
  • A whole-app audit that replaced invented and preview values with real sources or explicit empty states.
  • One set of project rules and docs shared by Claude, Codex and other AI tools.

Vital is not clinically validated, and its scores are wellness estimates, not medical measurements. There are no adoption, revenue or speed figures here. Direct Oura, WHOOP, Garmin, Fitbit or Withings connections aren’t available; those devices can share through Apple Health. Some audit items, such as interrupted-workout recovery, are still open.

A calm interface earns trust by showing the limits of what it knows. On Vital, the empty states took more design than the full ones, and they were worth it.

Next projectCefalo Design Platform