Apple Health (HealthKit) to FHIR
Read Apple Watch and Apple Health data with HealthKit in Swift, per local day, and write it to a FHIR R4 record: permissions, background sync, Apple's rules.
Apple Watch data lives in Apple Health on the paired iPhone, and the only way to read it is HealthKit, from an app on that iPhone, with the person's permission. To put it in a FHIR record, read daily statistics with HKStatisticsCollectionQuery, send the values to your backend, and write each metric as an Observation with a LOINC code and a UCUM unit. This page shows the Swift code (type-checked against the iOS 17 SDK), the permissions and rules Apple sets, and how the values become FHIR R4 Observations.
What is in Apple Health
Apple Health collects what the iPhone and Apple Watch measure — heart rate, resting heart rate, heart rate variability (SDNN), blood oxygen, respiratory rate, steps, energy, sleep stages, irregular rhythm notifications, ECG recordings — and what other apps write: Garmin Connect, Oura, Withings Health Mate, smart scales, blood pressure monitors, glucose meters. Reading Apple Health therefore covers more than the Apple Watch.
There is no HealthKit server or web API. Data stays on the device (and in the person's encrypted iCloud backup); your app reads it locally.
Permissions
- Turn on the HealthKit capability in Xcode and add
NSHealthShareUsageDescriptiontoInfo.plist(plusNSHealthUpdateUsageDescriptiononly if you write). - Ask for the types you need. People can allow some and refuse others, and HealthKit does not tell your app whether read access was refused: a refused type simply returns no data. Design the screens for "no data" rather than for an error.
- Ask at the moment the person connects, with a sentence explaining why, not at first launch.
Reading daily values in Swift
import Foundation
import HealthKit
/// One day of Apple Health data, ready to send to your backend.
struct DailyValue: Codable {
let metric: String // "heart_rate", "steps"
let day: String // "2026-10-01", the user's local day
let value: Double
let min: Double?
let max: Double?
}
let store = HKHealthStore()
let heartRate = HKQuantityType(.heartRate)
let steps = HKQuantityType(.stepCount)
func requestAccess() async throws {
// Read-only: the app never writes to Apple Health.
try await store.requestAuthorization(toShare: [], read: [heartRate, steps])
}
/// Daily statistics for the last `days` days, one value per local calendar day.
func dailyValues(_ type: HKQuantityType, metric: String, unit: HKUnit,
options: HKStatisticsOptions, days: Int) async throws -> [DailyValue] {
let calendar = Calendar.current
let end = calendar.startOfDay(for: Date()).addingTimeInterval(86_400)
let start = calendar.date(byAdding: .day, value: -days, to: end)!
let query = HKStatisticsCollectionQueryDescriptor(
predicate: HKSamplePredicate.quantitySample(type: type,
predicate: HKQuery.predicateForSamples(withStart: start, end: end)),
options: options,
anchorDate: start,
intervalComponents: DateComponents(day: 1))
let collection = try await query.result(for: store)
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd"
var out: [DailyValue] = []
collection.enumerateStatistics(from: start, to: end) { stats, _ in
let day = fmt.string(from: stats.startDate)
if options.contains(.cumulativeSum), let sum = stats.sumQuantity() {
out.append(DailyValue(metric: metric, day: day, value: sum.doubleValue(for: unit), min: nil, max: nil))
} else if let avg = stats.averageQuantity() {
out.append(DailyValue(metric: metric, day: day, value: avg.doubleValue(for: unit),
min: stats.minimumQuantity()?.doubleValue(for: unit),
max: stats.maximumQuantity()?.doubleValue(for: unit)))
}
}
return out
}
func collect() async throws -> [DailyValue] {
try await requestAccess()
let bpm = HKUnit.count().unitDivided(by: .minute())
let hr = try await dailyValues(heartRate, metric: "heart_rate", unit: bpm,
options: [.discreteAverage, .discreteMin, .discreteMax], days: 30)
let st = try await dailyValues(steps, metric: "steps", unit: .count(), options: .cumulativeSum, days: 30)
return hr + st // POST these to your backend, which writes them to the FHIR record
}
Why statistics queries rather than reading samples: HKStatisticsCollectionQuery returns per-day totals and averages that Apple Health has already de-duplicated across sources, so steps counted by both the iPhone and the Watch are not added twice. Reading raw samples and summing them yourself is the most common source of inflated step counts.
The days are the person's local calendar days (Calendar.current). Keep them that way all the way to the FHIR record, where the Observation's effectivePeriod is that local day with its time-zone offset.
Keeping the record up to date
- On open: read the days since the last successful sync each time the app comes to the foreground. This is the reliable baseline.
- In the background:
HKObserverQuerywithenableBackgroundDelivery(for:frequency:)wakes your app when new data arrives; it needs the HealthKit background delivery entitlement, and iOS decides the actual timing. - First connection: import a limited history (for example 30 days) rather than years, then move forward.
From Swift values to FHIR Observations
Send the DailyValue array to your backend and write one Observation per person, metric and day with a stable identifier, updating it when the same day comes again. The mapping table (HealthKit type → LOINC → UCUM), a validated JSON example and the TypeScript code that does this upsert, run against the platform, are on Wearable data in FHIR. The codes for the two metrics here are:
| HealthKit | LOINC | UCUM | Value per day |
|---|---|---|---|
heartRate |
8867-4 Heart rate | /min |
average, with minimum and maximum as components |
stepCount |
41950-7 Number of steps in 24 hour | {steps} |
total |
Apple's rules for health data
- App Review Guideline 5.1.3: health data may not be used for advertising or data mining, may not be shared with third parties without the person's consent, and personal health information may not be stored in iCloud.
- Your privacy policy must say what you read and why; Apple checks.
- Data written by your app to Apple Health is attributed to your app; data you read keeps its original source (
HKSource), which you should carry into the record as the source system.
Apple Health Records
Separately from fitness data, Apple Health can hold clinical records downloaded from participating health systems (mostly in the United States), and HealthKit exposes them as HKClinicalRecord objects that carry the original FHIR resource. Reading them needs its own permission and entitlement. They are a different integration from wearable data, and Anpheros Daily does not read them today.
Where Anpheros fits
The Anpheros Platform stores the result: the person's FHIR R4 record, with each wearable value as an Observation marked as device data, consent for other apps, provenance and an access log. It does not read Apple Health itself — that happens in your iOS app. Anpheros' own app, Anpheros Daily, reads Apple Health on iPhone (Plus and Max plans) and writes daily summaries to the record with the codes above.
Frequently asked questions
Can I read Apple Watch data from a server?
No. Apple Watch data is synchronised to Apple Health on the paired iPhone, and HealthKit can only be used by an app on that device. The app reads the data and sends it to your server.
How do I know if the person refused access to a HealthKit type?
You cannot, for read access: HealthKit hides it to protect privacy, and a refused type returns no data. Treat "no data" as a normal state and offer a way to review the permissions in the Health app.
Why are my step counts higher than in the Health app?
You are probably summing raw samples from several sources (iPhone and Watch). Use HKStatisticsCollectionQuery with .cumulativeSum, which applies Apple Health's own de-duplication.
Which LOINC code is Apple Watch heart rate variability?
Apple Health stores HRV as SDNN, which matches LOINC 80404-7. Health Connect stores RMSSD, a different statistic, so do not mix the two under one code.
Related
- Wearable data in FHIR: the mapping table and the backend code
- Health Connect to FHIR: the same on Android
- How to build a personal health record app
- How to build a healthcare app with Flutter