iOS Case Study
Elemental
A private supplement and medication log that pairs what you actually took with changes in your own health trends — processed entirely on device.
Version 1.0 iPhone, iOS 26+ Submitted to App Store review, September 2026
- Role
- Independent — product design, iOS engineering, release
- Stack
- Swift, SwiftUI, GRDB/SQLite, WidgetKit, App Intents, HealthKit, Vision
- Architecture
- Platform-neutral domain package behind a SwiftUI app layer
- Network surface
- None in the shipping app — no account, no server, no analytics SDK
01The constraint that shaped the build
A health log is only worth keeping if the person keeping it trusts where the data goes. Supplement and medication routines are exactly the kind of record people are reluctant to hand to an account system, so privacy here is an architectural property rather than a promise in a policy document.
That decision cascades. There is no server to move recognition to, so label parsing had to run on device. There is no backend to compute statistics, so the association analysis had to be pure, testable domain logic that runs locally and on a background task. There is no analytics SDK, so behaviour questions had to be answered by reading the code and the tests instead of a dashboard.
The result is a shipping app with a deliberately small surface: local storage, on-device recognition, read-only Apple Health access, and no network calls at all.
02What it does
Four root journeys. Today resolves the next scheduled intake and the current batch, and records taken or skipped. Products builds a product profile from a photographed label. Ledger holds the actual-intake history and the outcome records. Insights surfaces observed associations between a confirmed change in intake and what followed it.
03Architecture
Domain logic lives in ElementalCore, a platform-neutral Swift package organised by bounded context: ontology, supply, scheduling, intake, dosage, lab, outcomes, analysis and reference data. Core models are value types and Sendable, so the same logic runs in the app, in the widget extension and in tests without a simulator.
The SwiftUI layer is organised by user journey rather than by screen type, and shares a design-system module with the widget extension. Widgets and App Intents resolve the next dose through the same resolver the main app uses, and write through the same idempotent path — a check-in from the Home Screen cannot double-log the same scheduled entry.
Persistence is GRDB/SQLite in an App Group container that the app, the widget and App Intents all open. Schema changes go through explicit migrations, and a database created by an earlier private-container build migrates into the shared container without overwriting data that is already there.
Two decisions worth calling out
- History is append-only by construction. A schedule is stored as immutable, effective-dated revisions. Editing a plan creates a new revision that applies from that day forward; it never rewrites what the log says happened last month.
- "Skipped" is not "taken." Dispositions are stored separately from intake events, so a skipped dose can never be aggregated into exposure or silently counted as an intake.
04From a label photo to a product profile
Reading a Supplement Facts label is a good test case for on-device extraction, because the cost of a wrong answer is high and the source is messy: small type, unit noise, undisclosed blends, and marketing copy mixed with facts.
- CaptureCamera or photo library. The image is used in memory for recognition and is not retained as a separate copy.
- RecogniseVision OCR extracts text on device, with an optional Foundation Models enhancement when the device supports it.
- StructureRule-based parsing turns text into named ingredients, amounts and units rather than passing raw strings forward.
- NormaliseEntries are matched against a bundled catalog with synonyms and common dose ranges.
- Reject what cannot be trustedLow confidence, unknown ingredients, unit anomalies, impossible quantities, out-of-range amounts and undisclosed content are all flagged before saving.
- ConfirmThe user reviews and edits the parsed facts. Recognition proposes; the person decides. Nothing is saved unconfirmed.
Why this order matters. The interesting failure mode is not a recognition error — it is a recognition error that gets saved silently and corrupts months of downstream analysis. Treating the parser as an advisor with an explicit review gate protects the data that later analysis depends on.
05From logs to observations
The analysis asks a narrow question: when a change in actually recorded intake was confirmed, did anything shift in the outcome series around it? Everything about the design is in service of not overstating the answer.
- Exposure is built from records, not plans. Change detection classifies starts, interruptions, deactivations, dose changes, frequency changes and timing changes from confirmed intake events only.
- Noise is filtered before statistics. Changes within a three-day window collapse into a single transition identity, while small dose drift, timezone shifts and seven-day duplicate candidates are suppressed.
- Two-sample windows, qualified results. Before/after windows are compared per metric, and only results that clear interval, effect-size and threshold conditions are returned.
- An insight carries its own caveats. Each one exposes its sample window and limitations, and the copy distinguishes an ongoing observation from a qualified finding.
- Language is constrained on purpose. Insights describe associations, patterns and trends. They are not allowed to claim that a supplement or medication caused a change.
The domain layer covers schedule resolution, exposure-change detection, two-sample estimation and outcome association with pure unit tests, which is what makes the statistical behaviour reviewable without a device.
06Correctness details that decided the design
Several parts of the domain are small, unglamorous and load-bearing. Each of these exists because the naive version produces a wrong number that looks plausible:
- Dosage mass uses decimal fixed-point arithmetic, not binary floating point, so unit conversion does not accumulate representation error.
- Units are typed and converted explicitly; milligrams and micrograms are different quantities and never merge.
- Negative mass is rejected at construction, because a negative amount would silently cancel total exposure during aggregation.
- Same-day compatible records sum together; records in incompatible units never collapse into one exposure row.
- Aggregation ignores records outside the requested range, and the dominant time bucket is only chosen on a strict majority.
- Multi-table writes — product, ingredients, parse record, plan, intake, outcome, inventory, notification preferences — run in transactions.
- Formal insight identity survives a window upgrade, so a finding that matures from a 14-day to a 28-day window is not reported as a new one.
07Verification and release engineering
Verification is evidence-based: each release step is recorded with the command, the log and the resulting artifact, and nothing is marked done because it was intended to be.
Release path
- GitHub Actions builds and exports the signed release archive; local verification runs the package tests with Swift Testing.
- The App and Widget carry separate App Store distribution profiles and a shared App Group entitlement, and both signatures are checked recursively before upload.
- Builds are delivered to App Store Connect, exercised through TestFlight, and iterated against App Review feedback.
- Dedicated validators cover the app icon, the privacy-policy pages, the release contract and screenshot integrity, so compliance artifacts fail loudly rather than drifting.
On the review iteration. App Review feedback drove real fixes back into the shipping build — including validation behaviour when a user fills in an unrecognised ingredient by hand. Handling that loop is part of the work, not an interruption to it.
08What it deliberately does not do
An early version of a health product accumulates tempting features. These are out of scope on purpose, and the app is built and shipped without them:
- No diagnosis, dosage advice, interaction warnings or treatment guidance.
- No causal claims — an association is never presented as proof.
- No lab-report or blood-marker analysis.
- No account, sync, cloud storage or caregiver sharing.
- No writes to Apple Health; reads are read-only and optional.
- No advertising, tracking, analytics or payment SDKs.
- No iPad, Mac, watch, Android or web client.
- No claim of regulatory approval or medical-device status.
Drawing this line early kept the domain honest: every feature that survived had to justify itself against the analytical guarantees above.
09Status and links
Elemental 1.0 is complete, signed and delivered, and has been submitted to App Store review as of September 2026. The app is iPhone-only, requires iOS 26 or later, and contains no developer-operated network endpoint.
- Privacy policy — elemental-privacy.pages.dev
- Support — elemental-privacy.pages.dev/support
- Contact — hushiyebrady@gmail.com