healthkit

healthkit

熱門

使用 HealthKit 讀取、寫入與查詢 Apple 健康資料。涵蓋 HKHealthStore 授權、樣本查詢、統計查詢、用於圖表的統計集合查詢、儲存 HKQuantitySample 資料、背景傳送、使用 HKWorkoutSession 與 HKLiveWorkoutBuilder 的體能訓練階段、HKUnit 以及 HKQuantityTypeIdentifier 值。適用於整合 Apple 健康、顯示健康指標、記錄體能訓練或啟用背景健康資料傳送。

932星標
47分支
更新於 2026/7/15
SKILL.md
唯讀
名稱
healthkit
描述

使用 HealthKit 讀取、寫入與查詢 Apple 健康資料。涵蓋 HKHealthStore 授權、樣本查詢、統計查詢、用於圖表的統計集合查詢、儲存 HKQuantitySample 資料、背景傳送、使用 HKWorkoutSession 與 HKLiveWorkoutBuilder 的體能訓練階段、HKUnit 以及 HKQuantityTypeIdentifier 值。適用於整合 Apple 健康、顯示健康指標、記錄體能訓練或啟用背景健康資料傳送。

HealthKit

從 Apple 健康資料庫讀取與寫入健康和體能資料。涵蓋授權、查詢、寫入樣本、背景傳送與體能訓練階段。目標為 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 App 中可用。在 iPadOS 16 或更早版本上不可用,且可能受管理裝置政策限制。

import HealthKit

guard HKHealthStore.isHealthDataAvailable() else {
    // 此裝置上健康資料不可用或受限制。
    return
}

let healthStore = HKHealthStore()

建立單一 HKHealthStore 實例並在整個 App 中重複使用。它是執行緒安全的。如果 HealthKit 是選用的,請檢查 Xcode 產生的 UIRequiredDeviceCapabilities 中的 healthkit 項目,以免意外排除不支援的裝置。

授權

僅請求 App 真正需要的資料類型。App Review 會拒絕過度請求的 App。

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 不會透露讀取權限是否被授予或拒絕。如果使用者拒絕讀取權限,查詢只會傳回 App 成功儲存的樣本,這可能看起來像是空資料或部分資料。

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

switch status {
case .notDetermined:
    // 尚未請求——可以安全呼叫 requestAuthorization
    break
case .sharingAuthorized:
    // 使用者授予寫入權限
    break
case .sharingDenied:
    // 使用者拒絕寫入權限(讀取拒絕與「無資料」無法區分)
    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
}

// 從樣本中提取數值:
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 包含更新後的資料
    }
}

寫入資料

建立 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) 回傳視為儲存成功的閘門;只有成功後才回報成功或推進 App 狀態。若失敗,顯示錯誤並修正已知的授權、類型、單位、持續時間或輸入問題,然後再建立另一個樣本。若需要持久性證據,在健康 App 中進行有限查詢或檢查可作為整合測試檢查,但並非每次儲存後的必要生產環境讀取。

您的 App 只能刪除自己建立的樣本。來自其他 App 或 Apple Watch 的樣本是唯讀的。

背景傳送

註冊背景更新,以便在新資料到達時啟動您的 App。需要背景傳送授權。

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() }  // 必須呼叫以表示完成
    guard error == nil else { return }
    // 取得新資料、更新 UI 等
}
healthStore.execute(observerQuery)

頻率: .immediate.hourly.daily.weekly

在 App 啟動後立即設定觀察者查詢,然後對相同樣本類型呼叫一次 enableBackgroundDelivery。系統會持續保留註冊,在每個請求的頻率內最多喚醒 App 一次,並對某些類型(例如 iOS 上的每小時步數傳送)施加更嚴格的限制。背景傳送不支援模擬器;請在實體裝置上測試。

體能訓練階段

使用 HKWorkoutSessionHKLiveWorkoutBuilder 追蹤即時體能訓練。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())
}

// 請求結束;從委派的 .stopped 轉換進行最終化。
session.stopActivity(with: Date())

不要在請求停止後立即呼叫 endCollectionfinishWorkout。請等待階段委派的 .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 參考

// 基本單位
HKUnit.count()                              // 步數、計數
HKUnit.meter()                              // 距離
HKUnit.mile()                               // 距離(英制)
HKUnit.kilocalorie()                        // 能量
HKUnit.joule(with: .kilo)                   // 能量(SI)
HKUnit.gramUnit(with: .kilo)                // 質量(公斤)
HKUnit.pound()                              // 質量(英制)
HKUnit.percent()                            // 百分比

// 複合單位
HKUnit.count().unitDivided(by: .minute())   // 心率(bpm)
HKUnit.meter().unitDivided(by: .second())   // 速度(m/s)

// 前綴單位
HKUnit.gramUnit(with: .milli)               // 毫克
HKUnit.literUnit(with: .deci)               // 分升

常見錯誤

  1. 過度請求資料類型。 僅請求功能實際使用的讀取/寫入類型;廣泛的 HealthKit 權限表單是 App Review 的風險。
  2. 將讀取授權視為寫入授權。 您可以在儲存前檢查 .sharingAuthorized,但讀取拒絕受到隱私保護,看起來只會傳回 App 自己擁有的、空的或部分的結果。
  3. 跳過 isHealthDataAvailable() 在 HealthKit 存取前檢查,並處理不可用或受限制的資料庫而不崩潰。
  4. 在新的非同步程式碼中使用回呼查詢。 對於一次性讀取和統計,偏好使用非同步描述符,並將大型查詢保留在主執行緒之外。
  5. 忘記觀察者完成處理器。 務必呼叫處理器;遺漏完成可能會延遲或停止未來的背景傳送。
  6. 假設 .immediate 是立即的。 背景傳送受到系統限制,必須在實體裝置上測試。
  7. 對離散值使用累積統計。 將統計選項與資料類型匹配:步數/能量使用累積總和,心率、體重等類似樣本使用離散平均值/最小值/最大值。

審查清單

  • [ ] 在任何 HealthKit 存取前檢查 HKHealthStore.isHealthDataAvailable()
  • [ ] 授權中僅請求必要的資料類型
  • [ ] Info.plist 包含 NSHealthShareUsageDescription 和/或 NSHealthUpdateUsageDescription
  • [ ] 在 Xcode 專案中啟用 HealthKit 功能
  • [ ] 儲存前檢查寫入授權;讀取拒絕視為部分或空的查詢結果
  • [ ] 重複使用單一 HKHealthStore 實例(不為每個查詢建立)
  • [ ] 使用非同步查詢描述符而非回呼式查詢
  • [ ] 大型查詢不阻塞主執行緒
  • [ ] 統計選項與資料類型匹配(累積 vs. 離散)
  • [ ] 背景傳送搭配 App 啟動時的 HKObserverQuery 設定,並呼叫 completionHandler
  • [ ] 若使用 enableBackgroundDelivery,已啟用背景傳送授權
  • [ ] 背景傳送已在實體裝置上測試,並考慮頻率限制
  • [ ] 體能訓練停止等待委派的 .stopped 轉換後才執行 endCollectionfinishWorkout;僅在成功最終化後清除狀態
  • [ ] 處理體能訓練 API 可用性與即時心率感測器需求
  • [ ] 刪除操作僅針對 App 先前儲存的物件

參考資料