mcpField
H
healthkit

healthkit

github.com/dpearson2699开发1.2k3.9k 次安装13 周前更新
代码仓库

关于 healthkit

面向 Swift 开发者的 HealthKit 技能,介绍 Apple Health 数据的授权、读取、写入、后台更新和运动会话。内容聚焦现代异步查询及实现注意事项。

主要功能

  1. 01检查 HealthKit 可用性并请求授权
  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 实例,并在整个应用中重复使用。它是线程安全的。如果 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) 返回作为保存成功的判定依据;只有在这之后才报告成功或推进应用状态。如果失败,请显示错误,并在构造另一个样本前修正已知的授权、类型、单位、时长或输入问题。当需要持久化证据时,在 Health 应用中执行范围受限的查询或检查,有助于进行集成测试;但生产环境中每次保存后都进行读取并非强制要求。

应用只能删除自己创建的样本。其他应用或 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。系统会保留注册信息,最多按请求的频率唤醒应用一次, 并对某些类型施加更严格的上限,例如在 iOS 上按小时传输步数数据。 模拟器不支持后台传输;请在设备上进行测试。

锻炼会话

使用 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. 在新的异步代码中使用回调查询。 单次读取和统计优先使用异步描述符,并避免在主线程执行范围过广的查询。
  5. 忘记调用观察者完成处理程序。 务必调用该处理程序;遗漏完成调用可能会延迟或停止后续后台传递。
  6. 以为 .immediate 意味着立即传递。 后台传递受系统限制,必须在设备上测试。
  7. 对离散值使用累积统计。 根据数据类型选择统计选项:步数/能量使用累积总和,心率、体重及类似样本使用离散平均值/最小值/最大值。

审核清单

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

参考资料

评论

支持 Markdown