healthkit

healthkit

热门

使用 HealthKit 读取、写入和查询 Apple 健康数据。涵盖 HKHealthStore 授权、样本查询、统计查询、用于图表的统计集合查询、保存 HKQuantitySample 数据、后台投递、使用 HKWorkoutSession 和 HKLiveWorkoutBuilder 的锻炼会话、HKUnit 以及 HKQuantityTypeIdentifier 值。在集成 Apple 健康、显示健康指标、记录锻炼或启用后台健康数据投递时使用。

932Star
47Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
healthkit
description

使用 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. 如需后台投递,启用“后台投递”子能力

可用性检查

在调用其他 HealthKit API 之前,始终检查可用性。健康数据在 iOS、watchOS、visionOS、iPadOS 17+ 以及运行在 Vision Pro 上的 iOS 应用中可用。在 iPadOS 16 或更早版本上不可用,并可能受托管设备策略限制。

import HealthKit

guard HKHealthStore.isHealthDataAvailable() else {
    // 健康数据在此设备上不可用或受限。
    return
}

let healthStore = HKHealthStore()

创建一个 HKHealthStore 实例并在整个应用中重复使用。它是线程安全的。如果 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:
    // 尚未请求——可以安全调用 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) 返回视为保存成功的标志;只有在此之后才报告成功或推进应用状态。失败时,暴露错误并纠正已知的授权、类型、单位、持续时间或输入问题,然后再构造另一个样本。在需要持久性证据时,在健康应用中进行有限查询或检查作为集成测试检查是有用的,但并非每次保存后都必须进行生产读取。

您的应用只能删除自己创建的样本。来自其他应用或 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() }  // 必须调用以表示完成
    guard error == nil else { return }
    // 获取新数据,更新 UI 等
}
healthStore.execute(observerQuery)

频率: .immediate.hourly.daily.weekly

在应用启动后尽快设置观察者查询,然后对同一样本类型调用一次 enableBackgroundDelivery。系统会持久化注册,最多按请求的频率唤醒应用一次,并对某些类型(如 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)                   // 能量(国际单位制)
HKUnit.gramUnit(with: .kilo)                // 质量(千克)
HKUnit.pound()                              // 质量(英制)
HKUnit.percent()                            // 百分比

// 复合单位
HKUnit.count().unitDivided(by: .minute())   // 心率(bpm)
HKUnit.meter().unitDivided(by: .second())   // 速度(米/秒)

// 带前缀的单位
HKUnit.gramUnit(with: .milli)               // 毫克
HKUnit.literUnit(with: .deci)               // 分升

常见错误

  1. 过度请求数据类型。 仅请求功能实际使用的读写类型;广泛的 HealthKit 权限表是 App Review 的风险。
  2. 将读取授权视为写入授权。 您可以在保存前检查 .sharingAuthorized,但读取拒绝受隐私保护,看起来像是仅应用拥有的、空的或部分结果。
  3. 跳过 isHealthDataAvailable() 在访问 HealthKit 之前检查,并处理不可用或受限的存储而不崩溃。
  4. 在新的异步代码中使用回调查询。 对于一次性读取和统计,优先使用异步描述符,并将广泛查询保留在主 actor 之外。
  5. 忘记观察者完成处理程序。 始终调用处理程序;未完成的回调可能会延迟或停止未来的后台投递。
  6. 假设 .immediate 是立即的。 后台投递受系统限制,必须在设备上测试。
  7. 对离散值使用累积统计。 将统计选项与数据类型匹配:步数/能量使用累积总和,心率、体重等使用离散平均值/最小值/最大值。

审查清单

  • [ ] 在任何 HealthKit 访问前检查 HKHealthStore.isHealthDataAvailable()
  • [ ] 授权中仅请求必要的数据类型
  • [ ] Info.plist 包含 NSHealthShareUsageDescription 和/或 NSHealthUpdateUsageDescription
  • [ ] 在 Xcode 项目中启用 HealthKit 能力
  • [ ] 保存前检查写入授权;读取拒绝作为部分或空查询结果处理
  • [ ] 重复使用单个 HKHealthStore 实例(不每次查询创建)
  • [ ] 使用异步查询描述符而非基于回调的查询
  • [ ] 繁重查询不阻塞主线程
  • [ ] 统计选项与数据类型匹配(累积 vs 离散)
  • [ ] 后台投递与应用启动时的 HKObserverQuery 设置配对,并调用 completionHandler
  • [ ] 如果使用 enableBackgroundDelivery,启用后台投递授权
  • [ ] 在设备上测试后台投递,并考虑频率限制
  • [ ] 锻炼停止等待委托的 .stopped 转换后再执行 endCollectionfinishWorkout;仅在成功最终化后清除状态
  • [ ] 处理锻炼 API 可用性和实时心率传感器要求
  • [ ] 删除操作仅针对应用先前保存的对象

参考