# Apple Health (HealthKit) 到 FHIR

> 使用 Swift 中的 HealthKit 读取 Apple Watch 和 Apple Health 数据，按本地日期，并将其写入 FHIR R4 记录：权限、后台同步、Apple 的规则。

Source: https://developers.anpheros.com/zh/guides/apple-health-fhir

**Apple Watch 数据存储在配对 iPhone 上的 Apple Health 中，读取它的唯一方法是使用 HealthKit，从该 iPhone 上的应用程序读取，并获得用户的许可。** 要将其放入 FHIR 记录，请使用 `HKStatisticsCollectionQuery` 读取每日统计数据，将值发送到您的后端，并将每个指标作为具有 LOINC 代码和 UCUM 单位的 Observation 写入。本页展示了 Swift 代码（已针对 iOS 17 SDK 进行类型检查）、Apple 设置的权限和规则，以及这些值如何成为 FHIR R4 Observations。

## Apple Health 中的内容

Apple Health 收集 iPhone 和 Apple Watch 测量的数据 — 心率、静息心率、心率变异性 (SDNN)、血氧、呼吸频率、步数、能量、睡眠阶段、心律不齐通知、ECG 记录 — 以及其他应用程序写入的数据：Garmin Connect、Oura、Withings Health Mate、智能秤、血压计、血糖仪。因此，读取 Apple Health 可以涵盖比 Apple Watch 更多的数据。

没有 HealthKit 服务器或 Web API。数据保留在设备上（以及用户的加密 iCloud 备份中）；您的应用程序在本地读取它。

## 权限

1. 在 Xcode 中开启 **HealthKit** 功能，并添加 `NSHealthShareUsageDescription` 到 `Info.plist`（如果您写入数据，则额外添加 `NSHealthUpdateUsageDescription`）。
2. 请求您需要的类型。用户可以选择允许某些类型而拒绝其他类型，**HealthKit 不会告知您的应用读取权限是否被拒绝**：被拒绝的类型只会返回空数据。请设计“无数据”的界面，而不是错误界面。
3. 在用户连接时请求权限，并附带解释原因的句子，而不是在首次启动时请求。

## 在 Swift 中读取每日值

```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
}
```

为何使用统计查询而非读取样本：`HKStatisticsCollectionQuery` 返回的每日总计和平均值是 Apple Health 已经跨来源去重的结果，因此 iPhone 和 Watch 计算的步数不会重复累加。直接读取原始样本并自行求和是导致步数统计膨胀的最常见原因。

这些天是用户的本地日历日（`Calendar.current`）。在将其写入 FHIR 记录时，请保持此格式，其中 Observation 的 `effectivePeriod` 是带有其时区偏移量的本地日期。

## 保持记录最新

- **打开时：** 每次应用进入前台时，读取自上次成功同步以来的天数。这是可靠的基线。
- **在后台：** `HKObserverQuery` 使用 `enableBackgroundDelivery(for:frequency:)` 在新数据到达时唤醒您的应用；这需要 HealthKit 后台交付权限，并且 iOS 会决定实际的触发时间。
- **首次连接：** 导入有限的历史记录（例如 30 天），而不是多年，然后向前推进。

## 从 Swift 值到 FHIR 观测

将 `DailyValue` 数组发送到您的后端，并为每个人、每项指标和每一天写入一个具有稳定标识符的 Observation，当同一天再次到来时进行更新。映射表（HealthKit 类型 → LOINC → UCUM）、一个经过验证的 JSON 示例以及执行此 upsert 操作并在平台上运行的 TypeScript 代码，都可以在 [Wearable data in FHIR](https://developers.anpheros.com/zh/guides/wearable-data-fhir) 中找到。此处两个指标的代码是：

| HealthKit | LOINC | UCUM | 每日值 |
|---|---|---|---|
| `heartRate` | 8867-4 Heart rate | `/min` | 平均值，包含最小值和最大值作为组件 |
| `stepCount` | 41950-7 Number of steps in 24 hour | `{steps}` | 总计 |

## Apple 的健康数据规则

- App Review Guideline 5.1.3：健康数据不得用于广告或数据挖掘，未经用户同意不得与第三方共享，个人健康信息不得存储在 iCloud 中。
- 您的隐私政策必须说明您读取了什么以及为什么读取；Apple 会进行检查。
- 您的应用写入 Apple Health 的数据将归因于您的应用；您读取的数据将保留其原始来源 (`HKSource`)，您应将其作为源系统带入记录。

## Apple Health Records

除了健身数据之外，Apple Health 还可以存储从参与的医疗系统（主要在美国）下载的临床记录。HealthKit 将它们暴露为 `HKClinicalRecord` 对象，这些对象携带原始的 FHIR 资源。读取它们需要单独的权限和授权。这与可穿戴设备数据的集成不同，Anpheros Daily 目前不读取它们。

## Anpheros 的定位

Anpheros 平台存储结果：即用户的 FHIR R4 记录，其中每个可穿戴设备值都作为一个 Observation 记录，标记为设备数据、其他应用的同意、来源信息和访问日志。它本身不读取 Apple Health — 这部分由您的 iOS 应用完成。Anpheros 自有的应用 **Anpheros Daily** 会在 iPhone 上读取 Apple Health（仅限 Plus 和 Max 套餐），并将每日摘要写入记录中，使用上述代码。

## 常见问题解答

### 我可以从服务器读取 Apple Watch 数据吗？
不可以。Apple Watch 数据会同步到配对的 iPhone 上的 Apple Health，而 HealthKit 只能由该设备上的应用程序使用。应用程序读取数据并将其发送到您的服务器。

### 有 Apple Health 的 Web API 或 REST API 吗？
没有。Apple 不提供 Apple Health 的 Web 或 REST API：HealthKit 仅在 iPhone（或 Apple Watch）上的应用程序内运行。要在服务器上使用数据，您的应用程序需要获得用户许可后读取数据，然后将其发送，例如作为 FHIR Observations 发送到 Anpheros。

### 我需要 API 密钥或为 Apple Health API 付费吗？
无需密钥，也无需付费：HealthKit 是 iOS 的一部分。您需要在应用程序中启用 HealthKit 功能，为每种数据类型提供使用说明，并获得用户的许可；App Store 审核会检查您如何使用数据。

### 我如何知道用户是否拒绝访问 HealthKit 类型？
对于读取访问，您无法知道：HealthKit 会隐藏此信息以保护隐私，而拒绝的类型将不返回任何数据。将“无数据”视为正常状态，并提供一种在 Health 应用中查看权限的方式。

### 为什么我的步数计数比 Health 应用中的多？
您可能正在汇总来自多个来源（iPhone 和 Watch）的原始样本。请使用 `HKStatisticsCollectionQuery` 和 `.cumulativeSum`，它们会应用 Apple Health 自带的去重功能。

### Apple Watch 心率变异性的 LOINC 代码是什么？
Apple Health 将 HRV 存储为 SDNN，这与 LOINC 80404-7 匹配。Health Connect 存储 RMSSD，这是一个不同的统计量，因此不要将两者混淆在一个代码下。

## 相关资源

- [FHIR 中的可穿戴设备数据](https://developers.anpheros.com/zh/guides/wearable-data-fhir)：映射表和后端代码
- [Health Connect 到 FHIR](https://developers.anpheros.com/zh/guides/health-connect-fhir)：Android 上的相同内容
- [如何构建个人健康记录应用](https://developers.anpheros.com/zh/guides/personal-health-record-app)
- [如何使用 Flutter 构建医疗保健应用](https://developers.anpheros.com/guides/healthcare-app-flutter)
