mcpField
H
healthkit

healthkit

github.com/dpearson2699開発1.2k3.9k 回インストール13 週間前に更新
リポジトリ

healthkit について

Swift 開発者向けに、HealthKit による Apple Health データの認可、読み書き、バックグラウンド配信、ワークアウトセッションを解説するスキルです。最新の非同期クエリと実装上の確認事項を扱います。

主な機能

  1. 01利用可否の確認と認可の要求
  2. 02サンプルと統計データの読み取り
  3. 03健康・フィットネスサンプルの書き込み
  4. 04バックグラウンド配信の設定
  5. 05ライブワークアウトセッションの管理
  6. 06データ型と単位の確認

活用シーン

  1. 01歩数や心拍数などの健康データの読み取り
  2. 02時系列データの集計とグラフ化
  3. 03アプリが作成した健康サンプルの保存
  4. 04新しい健康データ到着時のアプリ更新
  5. 05ワークアウトセッションとライブデータの記録

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill healthkit

インストール後、Claude Code で/healthkitを入力して読み込まれたか確認します。

同梱ファイルevals/evals.jsonreferences/healthkit-patterns.md

詳細

HealthKit

Apple Health ストアから健康およびフィットネスデータを読み書きします。認可、クエリ、サンプルの書き込み、バックグラウンド配信、ワークアウトセッションを扱います。Swift 6.3/iOS 26 以降を対象としています。

目次

セットアップと利用可能な環境

プロジェクトの設定
  1. Xcode で HealthKit 機能を有効にします(エンタイトルメントが追加されます)
  2. Info.plist に NSHealthShareUsageDescription(読み取り)と NSHealthUpdateUsageDescription(書き込み)を追加します
  3. バックグラウンド配信を行う場合は、「Background Delivery」サブ機能を有効にします
利用可能かどうかの確認

ほかの HealthKit API を呼び出す前に、必ず利用可能かどうかを確認してください。健康データは iOS、watchOS、visionOS、iPadOS 17 以降、および Vision Pro で動作する iOS アプリで利用できます。iPadOS 16 以前では利用できず、管理対象デバイスのポリシーによって制限される場合があります。

import HealthKit

guard HKHealthStore.isHealthDataAvailable() else {
    // Health data is unavailable or restricted on this device.
    return
}

let healthStore = HKHealthStore()

HKHealthStore インスタンスを 1 つ作成し、アプリ全体で再利用してください。スレッドセーフです。HealthKit を任意機能にする場合は、サポート対象外のデバイスが意図せず除外されないよう、Xcode が生成する UIRequiredDeviceCapabilities の healthkit エントリーを確認してください。

認可

アプリが実際に必要とする型のみをリクエストしてください。過剰にリクエストするアプリは App Review で却下されます。

func requestAuthorization() async throws {
    let typesToShare: Set<HKSampleType> = [
        HKQuantityType(.stepCount),
        HKQuantityType(.activeEnergyBurned)
    ]

    let typesToRead: Set<HKObjectType> = [
        HKQuantityType(.stepCount),
        HKQuantityType(.heartRate),
        HKQuantityType(.activeEnergyBurned),
        HKCharacteristicType(.dateOfBirth)
    ]

    try await healthStore.requestAuthorization(
        toShare: typesToShare,
        read: typesToRead
    )
}
認可ステータスの確認

authorizationStatus(for:) は書き込み/共有の認可状態を報告します。HealthKit は読み取り権限が許可されたか拒否されたかを明らかにしません。ユーザーが読み取りアクセスを拒否した場合、クエリから返されるのはアプリが正常に保存したサンプルのみとなり、データが空または一部しかないように見えることがあります。

let status = healthStore.authorizationStatus(
    for: HKQuantityType(.stepCount)
)

switch status {
case .notDetermined:
    // Haven't requested yet -- safe to call requestAuthorization
    break
case .sharingAuthorized:
    // User granted write access
    break
case .sharingDenied:
    // User denied write access (read denial is indistinguishable from "no data")
    break
@unknown default:
    break
}

データの読み取り:サンプルクエリ

一度だけ読み取る場合は、HKSampleQueryDescriptor(async/await)を使用します。古いコールバックベースの HKSampleQuery よりも、デスクリプターを優先してください。

func fetchRecentHeartRates() async throws -> [HKQuantitySample] {
    let heartRateType = HKQuantityType(.heartRate)

    let descriptor = HKSampleQueryDescriptor(
        predicates: [.quantitySample(type: heartRateType)],
        sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)],
        limit: 20
    )

    let results = try await descriptor.result(for: healthStore)
    return results
}

// Extracting values from samples:
for sample in results {
    let bpm = sample.quantity.doubleValue(
        for: HKUnit.count().unitDivided(by: .minute())
    )
    print("\(bpm) bpm at \(sample.endDate)")
}

データの読み取り:統計クエリ

単一値の統計(合計、平均、最小、最大)を集計するには、HKStatisticsQueryDescriptor を使用します。

func fetchTodayStepCount() async throws -> Double? {
    let calendar = Calendar.current
    let startOfDay = calendar.startOfDay(for: Date())
    let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!

    let predicate = HKQuery.predicateForSamples(
        withStart: startOfDay, end: endOfDay
    )
    let stepType = HKQuantityType(.stepCount)
    let samplePredicate = HKSamplePredicate.quantitySample(
        type: stepType, predicate: predicate
    )

    let query = HKStatisticsQueryDescriptor(
        predicate: samplePredicate,
        options: .cumulativeSum
    )

    let result = try await query.result(for: healthStore)
    return result?.sumQuantity()?.doubleValue(for: .count())
}

データ型ごとのオプション:

  • 累積型(歩数、カロリー):.cumulativeSum
  • 離散型(心拍数、体重):.discreteAverage、.discreteMin、.discreteMax

データの読み取り:統計コレクションクエリ

時間間隔ごとにグループ化した時系列データ(グラフに最適)には、HKStatisticsCollectionQueryDescriptor を使用します。

func fetchDailySteps(forLast days: Int) async throws -> [(date: Date, steps: Double)] {
    let calendar = Calendar.current
    let endDate = calendar.startOfDay(
        for: calendar.date(byAdding: .day, value: 1, to: Date())!
    )
    let startDate = calendar.date(byAdding: .day, value: -days, to: endDate)!

    let predicate = HKQuery.predicateForSamples(
        withStart: startDate, end: endDate
    )
    let stepType = HKQuantityType(.stepCount)
    let samplePredicate = HKSamplePredicate.quantitySample(
        type: stepType, predicate: predicate
    )

    let query = HKStatisticsCollectionQueryDescriptor(
        predicate: samplePredicate,
        options: .cumulativeSum,
        anchorDate: endDate,
        intervalComponents: DateComponents(day: 1)
    )

    let collection = try await query.result(for: healthStore)
    var dailySteps: [(date: Date, steps: Double)] = []

    collection.statisticsCollection.enumerateStatistics(
        from: startDate, to: endDate
    ) { statistics, _ in
        let steps = statistics.sumQuantity()?
            .doubleValue(for: .count()) ?? 0
        dailySteps.append((date: statistics.startDate, steps: steps))
    }

    return dailySteps
}
長時間実行されるコレクションクエリ

results(for:)(複数形)を使用すると、新しいデータが届くたびに更新を発行する AsyncSequence を取得できます。

let updateStream = query.results(for: healthStore)

Task {
    for try await result in updateStream {
        // result.statisticsCollection contains updated data
    }
}

データの書き込み

HKQuantitySample オブジェクトを作成し、ストアに保存します。

func saveSteps(count: Double, start: Date, end: Date) async throws {
    let stepType = HKQuantityType(.stepCount)
    let quantity = HKQuantity(unit: .count(), doubleValue: count)

    let sample = HKQuantitySample(
        type: stepType,
        quantity: quantity,
        start: start,
        end: end
    )

    try await healthStore.save(sample)
}

try await healthStore.save(sample) が戻ることを保存成功の判定基準としてください。成功を報告したり、アプリの状態を進めたりするのは、その後にしてください。失敗した場合はエラーを通知し、別のサンプルを作成する前に、既知の認可、型、単位、期間、または入力の問題を修正してください。永続化の証拠が必要な場合は、範囲を限定したクエリやヘルスケアアプリでの確認が統合テストに役立ちますが、保存のたびに本番環境で読み取りを行う必要はありません。

アプリが削除できるのは、自分で作成したサンプルのみです。他のアプリや Apple Watch のサンプルは読み取り専用です。

バックグラウンド配信

バックグラウンド更新を登録すると、新しいデータが届いたときにアプリが起動します。バックグラウンド配信のエンタイトルメントが必要です。

func enableStepCountBackgroundDelivery() async throws {
    let stepType = HKQuantityType(.stepCount)

    try await healthStore.enableBackgroundDelivery(
        for: stepType,
        frequency: .hourly
    )
}

通知を処理するには、HKObserverQuery と組み合わせてください。必ず完了ハンドラーを呼び出してください。

let observerQuery = HKObserverQuery(
    sampleType: HKQuantityType(.stepCount),
    predicate: nil
) { query, completionHandler, error in
    defer { completionHandler() }  // Must call to signal done
    guard error == nil else { return }
    // Fetch new data, update UI, etc.
}
healthStore.execute(observerQuery)

頻度: .immediate、.hourly、.daily、.weekly

アプリの起動後すぐにオブザーバークエリを設定し、同じサンプルタイプに対して enableBackgroundDelivery を一度呼び出します。システムは登録を保持し、要求された頻度につき最大 1 回アプリを起動します。また、iOS での毎時の歩数配信など、一部のタイプにはより厳しい上限が適用されます。バックグラウンド配信は Simulator ではサポートされていないため、実機でテストしてください。

ワークアウトセッション

HKWorkoutSession と HKLiveWorkoutBuilder を使用して、進行中のワークアウトを記録します。 HKWorkoutSession は iOS/iPadOS 17 以降、visionOS 1 以降、watchOS 2 以降で利用できます。 HKLiveWorkoutBuilder は iOS/iPadOS 26 以降および watchOS 5 以降で利用できるため、古い iOS/iPadOS リリースをサポートする場合は、ライブビルダーのコードに条件を設定してください。

iPhone と iPad では、ライブ心拍数の収集にペアリング済みの外部心拍センサーが必要です。Apple Watch のセッションでは、高頻度の心拍数データを収集できます。ロック中の iPhone でワークアウトを行う場合、ロック画面にヘルス指標を表示する前に、システムのワークアウトデータアクセスのフローを考慮してください。

func startWorkout() async throws {
    let configuration = HKWorkoutConfiguration()
    configuration.activityType = .running
    configuration.locationType = .outdoor

    let session = try HKWorkoutSession(
        healthStore: healthStore,
        configuration: configuration
    )
    session.delegate = self

    let builder = session.associatedWorkoutBuilder()
    builder.dataSource = HKLiveWorkoutDataSource(
        healthStore: healthStore,
        workoutConfiguration: configuration
    )

    session.startActivity(with: Date())
    try await builder.beginCollection(at: Date())
}

// Request teardown; finalize from the delegate's .stopped transition.
session.stopActivity(with: Date())

停止を要求した直後に endCollection と finishWorkout を呼び出さないでください。セッションデリゲートの .stopped への遷移を待ち、その後 builder.endCollection(at:) を待ってから builder.finishWorkout() を呼び出します。両方の操作が完了してから、ワークアウトが保存されたことを報告し、セッションの状態をクリアしてください。エラーがスローされた場合はそれぞれ適切に処理し、ティアダウンをむやみに繰り返さないでください。デバイスがロックされている場合、finishWorkout() が成功してもワークアウトオブジェクトを返さないことがあります。そのため、結果が nil であることだけを理由に失敗と判断しないでください。

一時停止/再開、デリゲート処理、複数デバイス間のミラーリングを含むワークアウトのライフサイクル管理全般については、references/healthkit-patterns.md を参照してください。

一般的なデータタイプ

HKQuantityTypeIdentifier
識別子カテゴリ単位
.stepCountフィットネス.count()
.distanceWalkingRunningフィットネス.meter()
.activeEnergyBurnedフィットネス.kilocalorie()
.basalEnergyBurnedフィットネス.kilocalorie()
.heartRateバイタル.count()/.minute()
.restingHeartRateバイタル.count()/.minute()
.oxygenSaturationバイタル.percent()
.bodyMass体組成.gramUnit(with: .kilo)
.bodyMassIndex体組成.count()
.height体組成.meter()
.bodyFatPercentage体組成.percent()
.bloodGlucose検査.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
HKCategoryTypeIdentifier

一般的なカテゴリタイプ:.sleepAnalysis、.mindfulSession、.appleStandHour

HKCharacteristicType

読み取り専用のユーザー特性には、.dateOfBirth、.biologicalSex、 .bloodType、.fitzpatrickSkinType、.wheelchairUse、.activityMoveMode があります。

HKUnit リファレンス

// Basic units
HKUnit.count()                              // Steps, counts
HKUnit.meter()                              // Distance
HKUnit.mile()                               // Distance (imperial)
HKUnit.kilocalorie()                        // Energy
HKUnit.joule(with: .kilo)                   // Energy (SI)
HKUnit.gramUnit(with: .kilo)                // Mass (kg)
HKUnit.pound()                              // Mass (imperial)
HKUnit.percent()                            // Percentage

// Compound units
HKUnit.count().unitDivided(by: .minute())   // Heart rate (bpm)
HKUnit.meter().unitDivided(by: .second())   // Speed (m/s)

// Prefixed units
HKUnit.gramUnit(with: .milli)               // Milligrams
HKUnit.literUnit(with: .deci)               // Deciliters

よくある間違い

  1. 要求するデータ型が多すぎる。 機能で実際に使用する読み取り/書き込みの型のみを要求してください。広範な HealthKit の権限シートは、App Review で問題となるリスクがあります。
  2. 読み取りの認可を、書き込みの認可と同じように扱う。 保存前に .sharingAuthorized を確認できますが、読み取りの拒否はプライバシー保護のため確認できず、アプリが所有するデータのみ、空の結果、または一部の結果として表示されます。
  3. isHealthDataAvailable() を省略する。 HealthKit にアクセスする前に確認し、ストアが利用不可または制限されている場合もクラッシュしないように処理してください。
  4. 新しい非同期コードでコールバッククエリを使用する。 1 回限りの読み取りや統計には非同期ディスクリプターを優先し、広範なクエリはメインアクター上で実行しないでください。
  5. オブザーバーの完了ハンドラーを忘れる。 必ずハンドラーを呼び出してください。呼び出し忘れがあると、今後のバックグラウンド配信が遅延したり、停止したりすることがあります。
  6. .immediate が即時を意味すると考える。 バックグラウンド配信にはシステムによる上限があり、デバイス上でテストする必要があります。
  7. 離散値に累積統計を使用する。 統計オプションはデータ型に合わせてください。歩数やエネルギーには累積合計、心拍数や体重などのサンプルには離散的な平均値/最小値/最大値を使用します。

レビューチェックリスト

  • HealthKit にアクセスする前に HKHealthStore.isHealthDataAvailable() を確認している
  • 認可で必要なデータ型のみを要求している
  • Info.plist に NSHealthShareUsageDescription および/または NSHealthUpdateUsageDescription が含まれている
  • Xcode プロジェクトで HealthKit の機能が有効になっている
  • 保存前に書き込みの認可を確認し、読み取りの拒否を部分的な結果または空のクエリ結果として処理している
  • 単一の HKHealthStore インスタンスを再利用している(クエリごとに作成していない)
  • コールバックベースのクエリではなく、非同期クエリディスクリプターを使用している
  • 重いクエリがメインスレッドをブロックしていない
  • 統計オプションがデータ型(累積値と離散値)に合っている
  • バックグラウンド配信とアプリ起動時の HKObserverQuery の設定を組み合わせ、completionHandler を呼び出している
  • enableBackgroundDelivery を使用する場合、バックグラウンド配信のエンタイトルメントが有効になっている
  • バックグラウンド配信をデバイス上でテストし、頻度の上限を考慮している
  • ワークアウトの停止時、endCollection と finishWorkout の前にデリゲートの .stopped への遷移を待機し、正常に確定した後にのみ状態をクリアしている
  • ワークアウト API の利用可能状況と、ライブ心拍センサーの要件に対応している
  • 削除操作の対象を、以前にアプリが保存したオブジェクトのみにしている

参考資料

コメント