Mac
Reads an Apple Health export. No dependencies and no developer account: it signs ad-hoc and never links HealthKit.
iPhone
Reads HealthKit directly, including lab results from connected providers. Never writes to Health.
Build
Both projects are generated with XcodeGen.Why two apps
HealthKit compiles on macOS, but a Mac has nothing to read.HKHealthStore.isHealthDataAvailable() returns false, and there is no health database, no Health app and no healthd. Health data lives on the iPhone and does not sync to a Mac.
So the Mac app imports the export Health produces: Health → your profile picture → Export All Health Data, then drop the zip on the app.
On a phone, all of that is ceremony around data already sitting in the store. The iPhone app asks HealthKit instead. That also brings provider lab results within reach: HealthKit returns the same FHIR Observation resources that an export writes into clinical-records/.
Shared code
Two apps that both proposehrv_60_day_mean must propose the same number. The code that decides what a value is lives in shared/ and is compiled by both targets, not copied.
Only the parts that really differ are per-platform: the Mac’s zip handling and
export.xml parser, and the iPhone’s HealthKit queries.
What gets read
The mapping is deliberately small. It covers HealthKit types that are biomarkers in the catalogue’s sense, meaning a value with an optimal band to grade against. Step counts and workout minutes are real data, but nothing can grade them, so they are counted and dropped. Each type gets the reduction its biomarker asks for:
Five more mappings are written but inert, because their biomarkers are not in the catalogue yet:
bmi, respiratory_rate, heart_rate_recovery, body_fat_pct and waist_circumference. They wait on pending migrations. Until then, review lists them as unplaceable, so the gap is visible.
The iPhone app also reads labResultRecord and vitalSignRecord from the clinical store. The Mac app reads clinical-records/*.json from the export. Where a lab result and a wearable both answer for one biomarker, the lab wins, because it measured the thing directly.
Reduction windows
On both platforms, a window ends at the newest sample, not today. If you left your watch off for three weeks, you still get a real 30-day median. Anchoring on today would quietly shrink the window and change the number without saying so.Mac: reading a large export
export.xml is routinely hundreds of megabytes. Foundation’s XMLParser can’t handle that efficiently: it holds the whole input in libxml2, so memory tracks file size no matter how it is fed.
The Mac app reads fixed-size chunks and matches records line by line. The format supports this: the exporter writes one element per line, and a record with <MetadataEntry> children still declares every attribute on its opening line.
It parses 1,000,000 records in 2.6 seconds.
The
autoreleasepool inside the chunk loop is load-bearing. FileHandle returns an autoreleased Data for each chunk. Without draining the pool, every chunk stays alive until the parse ends, which rebuilds a copy of the whole file.Tests
Import/ has no UI dependencies, so the pipeline runs headless. The harness builds a synthetic export full of traps:
- A 60-day mean that a naive all-history average gets wrong
- A resting heart rate with an absurd outlier
- SpO₂ stored as a 0–1 fraction
- A VO₂ max whose latest reading is worse than an older one
- 200,000 step records that must never be proposed
- A non-numeric culture result that must be surfaced, not forced onto a near match
iPhone: permissions
Declined reads look like empty data
HealthKit never reports a declined read.authorizationStatus only describes sharing, and a refused read looks exactly like an empty store, so an app can’t infer what it wasn’t shown. The app can’t honestly say “denied”. Its empty state points to Settings → Privacy & Security → Health → Aletheia instead.
Clinical records are guarded, not caught
Clinical records are requested in a separate call, and only whensupportsHealthRecords() returns true. Asking for a clinical type without the Health Records entitlement throws an Objective-C exception that Swift’s catch can’t see, and the app crashes. Without the entitlement, the review screen says clinical records are unavailable.
For the same reason, units are built from typed HKUnit constructors rather than parsed from strings. HKUnit(from:) also raises an uncatchable exception on input it can’t read.
Purpose strings must be real sentences
iOS validatesNSHealthShareUsageDescription. A placeholder like “Probe.” aborts the app on launch.
History range on iOS 26 and later
The permission sheet now asks how far back to share: Past 30 Days or All Recorded Data. Choosing 30 days silently truncates every window, so a 60-day mean becomes a 30-day mean under the same name. Choose All Recorded Data.No background sync
HKObserverQuery with background delivery would let the app save new samples as they arrive, without anyone reviewing them. That is exactly what the review step exists to prevent. Reading only happens when you ask for it.
iPhone: signing
HealthKit is an entitled capability, so a phone build needs a development team.Aletheia.entitlementsincludescom.apple.developer.healthkit. SetDEVELOPMENT_TEAMinproject.yml, or pick a team in Xcode, and automatic signing provisions it.com.apple.developer.healthkit.access(Health Records) is commented out, so the default build signs with any account. It is a restricted capability that must be enabled on the App ID. Once it is, uncomment it and the clinical path starts returning documents with no code change.
iPhone: testing
- Simulator
- Phone
The simulator exercises sign-in, the record, and the full read → review → save path with data you enter yourself.Then add data in the simulator’s Health app: Browse → a metric → Add Data. Blood pressure, blood glucose, blood oxygen, resting heart rate and VO₂ max can all be entered by hand. Add several readings across different days for anything with a window. One reading makes a 30-day median that is just that reading.The simulator can’t show provider lab results, because
supportsHealthRecords() is false.Verified
- Every source type-checks under Swift 6 strict concurrency. Both apps build for device and simulator SDKs (Xcode 26.4 / iOS 26.4 and Xcode 27 beta / iOS 27).
- Sign-in reaches Supabase Auth and maps errors through the shared client.
- All seven reductions give the right answer on the fixture in
ios/Fixture: resting heart rate 62 from 29 in-window readings (outlier absorbed by the median, 20 older readings excluded), HRV 60.0 ms from 40 of 60 samples, blood oxygen 97 from a 0.97 fraction, VO₂ max 38.5 rather than an older, better 42, blood pressure 118/76, and glucose 92.
Still to do
- The record tab shows
dashboard.htmlin aWKWebViewon both platforms, so there is one implementation of the figure. A native body map is not built yet. - Blood pressure arrives as two separate quantity types and is proposed as two values. Reading
HKCorrelationTypeIdentifierBloodPressurewould keep each systolic reading paired with its diastolic. - The app could warn when its oldest visible sample is suspiciously recent, which would catch the 30-day permission choice.
- There is no Apple Watch target. A complication showing the record’s worst status is the obvious next step.
- The Mac app matches clinical records by name only. LOINC codes are more reliable, but only worth adding from a verified table.
