A privacy-focused iOS app that exports Apple HealthKit data to CSV files. All data processing happens entirely on-device — no health data is ever sent to external servers.
- Privacy-first design: All processing on-device, no analytics, no tracking, no accounts
- Multiple health metrics: Export Weight, Steps, Blood Glucose (mg/dL), selected clinical lab panels, and selected vitals
- Flexible date ranges: Last X days, last X records, custom date range, or all data
- Unit preferences: Configure weight units (kg/lbs) and temperature (°C/°F); defaults to US units (Fahrenheit, lbs)
- Selectable date format: Choose from 5 date formats including ISO8601 (UTC),
yyyy-MM-dd HH:mm:ss,MM/dd/yyyy HH:mm:ss, and more - Sort order: Export rows in ascending (oldest first) or descending (newest first) order
- CSV export: Save directly to Files app via
.fileExporter() - Welcome screen with navigation to data selection and settings
- Settings persistence: Unit preferences are automatically saved
- Clinical records support: Lab-result export requires Clinical Health Records capability and permission
- Memory-optimized: Health data is cleared from memory immediately after export
|
Splash Screen |
Metric Selector |
Settings |
- Open
HealthExporter.xcodeprojin Xcode - Ensure HealthKit is enabled in Signing & Capabilities
- If using lab-result export, enable Clinical Health Records capability (see lab result docs for details)
- Build and run on a physical device (HealthKit has limited simulator support)
- Launch the app (splash screen displays briefly)
- (Optional) Tap the gear icon to configure unit preferences or view the privacy policy
- Tap "Next" to go to the data selection screen
- Select metrics to export (Weight, Steps, Blood Glucose, lab sections, or vitals)
- Choose a date range option (last X days, last X records, date range, or all data)
- Tap "Save..." to save to Files
The exported CSV includes the following columns:
| Column | Description |
|---|---|
| Date | Timestamp in user-selected format (see below) |
| Metric | Type of measurement (Weight, Steps, Blood Glucose, lab result, or vital sign) |
| LOINC | LOINC code found in the lab record (e.g., 4548-4); empty for weight, steps, glucose, and vitals |
| Value | Numeric value with metric-specific precision |
| Unit | Unit of measurement (kg, lbs, steps, mg/dL, %, etc.) |
| Source | The app or device that recorded the data (e.g., Withings, Apple Watch) |
| Meal Time | Blood glucose only: Before Meal, After Meal, or empty when the reading has no meal time. Empty for all other metrics |
| Format | Example |
|---|---|
yyyy-MM-dd HH:mm:ss (default) |
2026-01-09 10:30:00 |
| ISO8601 (UTC) | 2026-01-09T10:30:00Z |
yyyy/MM/dd HH:mm:ss |
2026/01/09 10:30:00 |
MM/dd/yyyy HH:mm:ss |
01/09/2026 10:30:00 |
dd MMM yyyy HH:mm:ss |
09 Jan 2026 10:30:00 |
Example:
### HealthExporterCSV: data exported as-is from Apple Health.
### No warranty of accuracy or completeness or fitness for any purpose.
### Not a medical record. Verify with your healthcare provider before any clinical use.
Date,Metric,LOINC,Value,Unit,Source,Meal Time
2026-01-09 10:30:00,Weight,,185.50,lbs,Withings,
2026-01-09 11:00:00,Steps,,5432,steps,Apple Watch,
2026-01-09 07:15:00,Blood Glucose,,98,mg/dL,MyFitnessPal,Before Meal
2026-01-09 14:30:00,Blood Glucose,,145,mg/dL,MyFitnessPal,After Meal
2026-01-15 14:30:00,Hemoglobin A1C,4548-4,7.50,%,Apple Health,
2026-01-15 14:31:00,Total Cholesterol,2093-3,184,mg/dL,Apple Health,
2026-01-15 14:32:00,Blood Pressure Systolic,,120,mmHg,Apple Health,
2026-01-15 14:32:00,Blood Pressure Diastolic,,80,mmHg,Apple Health,
2026-01-15 14:33:00,Oxygen Saturation,,98.5,%,Apple Health,
Each export starts with three ### -prefixed disclaimer lines so common CSV parsers can be configured to skip them as comments. The LOINC column carries the LOINC code for clinical-record lab results and is left empty for HealthKit-native metrics (weight, steps, glucose, vitals). The Meal Time column comes from the meal-time tag on a blood glucose reading; every other row leaves it empty, so each row has seven fields.
Data can be sorted ascending (oldest first) or descending (newest first) within each metric type. Filename format: HealthExporter_YYYY-MM-DD_HHMMSS.csv.
- iOS 26+
- Physical iOS device (for full HealthKit functionality)
- HealthKit access permission
- Clinical Health Records capability and user permission (for lab-result export)
Unit tests run in GitHub Actions on every push to main and every pull request from a branch in this repo. The iOS Tests workflow (.github/workflows/ios-tests.yml) runs on the self-hosted Mac runner ([self-hosted, macOS, ARM64], MacMiniM6_01) through ci/run_ios_tests.sh. Pull requests from forks do not run on the self-hosted runner. See docs/TESTING.md for the full testing guide.
| Test file | Coverage |
|---|---|
CSVGeneratorTests.swift |
CSV generation for all metrics, unit conversion, formatting |
DateRangeOptionTests.swift |
Date range enum cases and display names |
HealthMetricConfigTests.swift |
Metric availability, LOINC codes |
GlucoseSampleTests.swift |
Blood glucose filtering (values >= 20 accepted, < 20 rejected) |
Hemoglobin A1C export has been verified working end-to-end on a physical device with Clinical Health Records enabled. Additional lab panels use the same registry-driven clinical-records pipeline and are covered by unit tests; validate new lab records on a physical device before release. Vitals use HealthKit quantity samples and should also be verified on device before release. See docs/a1c/ for lab implementation details.
Every pull request merged to main cuts a new version and uploads it to TestFlight. Nothing runs from a laptop. The chain:
- A merge to
mainstartsbump-build-number.ymlon the Linux runner. It bumps the build number and the patch version, commits the bump, pushes avX.Y.Ztag, creates the GitHub release, and dispatchesrelease-testflight.ymlon the tag. release-testflight.ymlruns on the Mac runner. It tests, archives, exports a signed IPA and uploads it to internal TestFlight.
| Workflow | Trigger | Runner | What it does |
|---|---|---|---|
.github/workflows/bump-build-number.yml |
PR merge to main |
[self-hosted, Linux, X64] |
Increments CURRENT_PROJECT_VERSION and the MARKETING_VERSION patch component, commits the bump, pushes a vX.Y.Z tag, creates the GitHub release with generated notes, and dispatches the TestFlight upload on the tag |
.github/workflows/release-testflight.yml |
Dispatch from the bump workflow, or by hand | [self-hosted, macOS, ARM64] (MacMiniM6_01) |
Tests, archives, exports a signed IPA via ci/ExportOptions.plist, and uploads it to internal TestFlight |
.github/workflows/publish-release.yml |
v* tag pushed by hand |
[self-hosted, Linux, X64] |
Fallback: creates the GitHub release for a tag that did not come from the bump workflow |
A tag that the bump workflow pushes with GITHUB_TOKEN does not trigger other workflows. That is why the bump workflow creates the release and dispatches TestFlight itself.
Rebuild and upload a specific tag by hand. App Store Connect rejects a build number that it already has, so this only helps when the earlier upload failed:
gh workflow run release-testflight.yml --repo evanwtf/HealthExporter --ref vX.Y.ZDry run: test, archive and export a signed IPA, but do not upload it:
gh workflow run release-testflight.yml --repo evanwtf/HealthExporter -f upload=falseThe TestFlight workflow needs these secrets. Its "Check required secrets" step fails the job early if one is missing.
- Repository secrets
ASC_API_KEY_ID,ASC_API_ISSUER_IDandASC_API_KEY_P8: an App Store Connect API key with the App Manager role. One team key serves all the team's apps. - Organization secret
MACOS_KEYCHAIN_PASSWORD: the password for the runner's login keychain. Its selected repositories must includeHealthExporter.
The archive signs automatically for development. The export signs manually (ci/ExportOptions.plist) with the runner's local Apple Distribution identity for team 94S5PZTVPY and the HealthExporter App Store profile. The workflow downloads that profile by name through the API key on every run (ci/install_profile.swift, read-only). An Admin creates or renews the profile once a year in the Apple Developer portal; nothing is installed on the runner by hand. See docs/macos-runner-recovery.md when signing or runner state drifts.
HealthExporter/
├── HealthExporter/
│ ├── HealthExporter.xcodeproj/
│ ├── HealthExporter.entitlements
│ └── HealthExporter/
│ ├── HealthExporterApp.swift # App entry point, NavigationStack
│ ├── LaunchView.swift # Splash screen with spinner + settings access
│ ├── DataSelectionView.swift # Metric selection & export UI
│ ├── SettingsView.swift # Unit/format preferences & test data
│ ├── PrivacyPolicy.swift # Policy text; matches docs/privacy-policy.md
│ ├── PrivacyPolicyView.swift # Shows the policy and disclaimer
│ ├── HealthKitManager.swift # HealthKit auth & data fetching
│ ├── HealthMetricConfig.swift # Base metric registry
│ ├── LabMetricRegistry.swift # Clinical lab panel registry
│ ├── VitalMetricRegistry.swift # HealthKit vital metric registry
│ ├── HealthSampleTypes.swift # Glucose, LOINC constants, FHIR parsing
│ ├── CSVGenerator.swift # CSV generation & unit conversion
│ ├── CSVFile.swift # Transferable CSV for the Files picker
│ ├── SettingsManager.swift # UserDefaults persistence
│ ├── DateRangeOption.swift # Date range selection enum
│ ├── ExportError.swift # Localized error types
│ └── Assets.xcassets/
├── HealthExporterTests/
│ ├── CSVGeneratorTests.swift
│ ├── DateRangeOptionTests.swift
│ ├── HealthMetricConfigTests.swift
│ └── GlucoseSampleTests.swift
├── docs/
│ ├── TESTING.md
│ ├── app-store-review.md
│ ├── privacy-policy.md
│ ├── index.md
│ └── a1c/
│ ├── QUICK_REFERENCE.md
│ └── IMPLEMENTATION_GUIDE.md
├── .github/workflows/ # ios-tests, bump-build-number, publish-release, release-testflight
├── ci/
│ ├── ExportOptions.plist # Export options (manual signing)
│ ├── install_profile.swift # Downloads the App Store profile by name
│ └── run_ios_tests.sh # Simulator unit tests, used by CI
└── README.md
The full privacy policy and the disclaimer are at evanwtf.github.io/HealthExporter/privacy-policy. The app shows the same text in Settings. In short:
- Everything happens on your iPhone. The app has no servers, no accounts, no analytics and no tracking.
- The app reads only the Apple Health data types you select, and only when you start an export.
- The exported CSV file is plain text. You choose where to save it, and after that the app cannot see it.
- The app is a data export utility, not medical advice, and it comes with no warranty.
The source of the policy is HealthExporter/HealthExporter/PrivacyPolicy.swift and docs/privacy-policy.md. PrivacyPolicyTests checks that the two say the same thing, so change both together.