# Apple Health (HealthKit) から FHIR へ

> Swift を使用して Apple Watch および Apple Health データを HealthKit で読み込み、ローカルの日付ごとに FHIR R4 レコードに書き込む方法：権限、バックグラウンド同期、Apple のルール。

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

**Apple Watch のデータは、ペアリングされた iPhone 上の Apple Health に保存されており、それを読み込む唯一の方法は、その iPhone 上のアプリから、ユーザーの許可を得て HealthKit を使用することです。** FHIR レコードに格納するには、`HKStatisticsCollectionQuery` を使用して日次統計情報を読み込み、その値をバックエンドに送信し、各メトリックを LOINC コードと UCUM 単位を持つ Observation として書き込みます。このページでは、Swift コード（iOS 17 SDK に対して型チェック済み）、Apple が設定する権限とルール、および値が FHIR R4 Observation になる方法を示します。

## Apple Health の概要

Apple Health は、iPhone および Apple Watch が測定した心拍数、安静時心拍数、心拍変動 (SDNN)、血中酸素濃度、呼吸数、歩数、消費カロリー、睡眠ステージ、不規則な心拍の通知、心電図記録といったデータと、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 Observationへ

`DailyValue` 配列をバックエンドに送信し、人物、メトリック、および日付ごとに1つのObservationを作成します。この際、安定した識別子を使用し、同じ日付が再度来た場合に更新します。マッピングテーブル（HealthKitタイプ → LOINC → UCUM）、検証済みのJSON例、およびこれを実行するTypeScriptコードは、[Wearable data in FHIR](https://developers.anpheros.com/ja/guides/wearable-data-fhir) にあります。ここで使用されている2つのメトリックのコードは以下の通りです。

| HealthKit | LOINC | UCUM | 1日あたりの値 |
|---|---|---|---|
| `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 のヘルスデータに関するルール

- App Review Guideline 5.1.3: ヘルスデータは広告やデータマイニングに使用してはならず、本人の同意なしに第三者と共有してはならず、個人健康情報は iCloud に保存してはなりません。
- プライバシーポリシーには、何を読み取り、その理由を明記する必要があります。Apple が確認します。
- アプリが Apple Health に書き込むデータは、アプリに帰属します。読み取るデータは、元のソース (`HKSource`) を維持します。このソースは、ソースシステムとしてレコードに引き継ぐ必要があります。

## Apple Health Records

フィットネスデータとは別に、Apple Health は参加している医療システム（主に米国）からダウンロードされた臨床記録を保持できます。HealthKit は、元の FHIR リソースを保持する `HKClinicalRecord` オブジェクトとしてそれらを公開します。これらの読み取りには、独自の権限とエンタイトルメントが必要です。これらはウェアラブルデータとは異なる統合であり、Anpheros Daily は現在それらを読み取っていません。

## Anpheros の位置づけ

Anpheros Platform は、個人の FHIR R4 レコードの結果を保存します。各ウェアラブル値は、デバイスデータとしてマークされた Observation、他のアプリへの同意、来歴、およびアクセスログとして記録されます。Anpheros は Apple Health 自体を読み取るわけではありません。これは、お客様の iOS アプリで行われます。Anpheros 独自のアプリである **Anpheros Daily** は、iPhone (Plus および Max プラン) 上の Apple Health を読み取り、日次サマリーを上記のコードとともにレコードに書き込みます。

## よくある質問

### Apple Watchのデータをサーバーから読み取れますか？
いいえ。Apple WatchのデータはペアリングされたiPhone上のApple Healthに同期され、HealthKitはそのデバイス上のアプリからのみ使用できます。アプリがデータを読み取り、サーバーに送信します。

### Apple HealthのWeb APIまたはREST APIはありますか？
いいえ。AppleはApple Health用のWeb APIまたはREST APIを提供していません。HealthKitは、個人のiPhone（またはApple Watch）上のアプリ内でしか実行されません。サーバーでデータを使用するには、アプリが本人の許可を得てデータを読み取り、例えばFHIR ObservationsとしてAnpherosに送信します。

### Apple Health APIにAPIキーは必要ですか、または料金はかかりますか？
キーも料金も不要です。HealthKitはiOSの一部です。アプリでHealthKitケーパビリティ、各データ型の使用目的の説明、および本人の許可が必要です。App Storeの審査では、データの使用方法がチェックされます。

### HealthKitタイプのアクセスを本人が拒否したことをどうすればわかりますか？
読み取りアクセスについては、わかりません。HealthKitはプライバシー保護のためにこれを隠蔽しており、拒否されたタイプはデータを返しません。「データなし」を通常の状態として扱い、Healthアプリで権限を確認する方法を提供してください。

### ステップカウントがHealthアプリよりも多いのはなぜですか？
おそらく、iPhoneとWatchなど、複数のソースからの生のサンプルを合計していると思われます。Apple Health独自の重複排除を適用する`.cumulativeSum`とともに`HKStatisticsCollectionQuery`を使用してください。

### Apple Watchの心拍変動に対応するLOINCコードは何ですか？
Apple HealthはHRVをSDNNとして保存しており、これはLOINC 80404-7に一致します。Health Connectは異なる統計であるRMSSDを保存するため、1つのコードの下で両者を混同しないでください。

## 関連情報

- [FHIRにおけるウェアラブルデータ](https://developers.anpheros.com/ja/guides/wearable-data-fhir): マッピングテーブルとバックエンドコード
- [Health Connect から FHIR へ](https://developers.anpheros.com/ja/guides/health-connect-fhir): Android 版も同様
- [個人健康記録アプリの構築方法](https://developers.anpheros.com/ja/guides/personal-health-record-app)
- [Flutter を使用したヘルスケアアプリの構築方法](https://developers.anpheros.com/ja/guides/healthcare-app-flutter)
