healthkit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HealthKit

HealthKit

Read and write health and fitness data from the Apple Health store. Covers authorization, queries, writing samples, background delivery, and workout sessions. Targets Swift 6.2 / iOS 26+.
从Apple Health存储中读取和写入健康与健身数据。内容涵盖授权、查询、写入样本、后台数据推送以及运动会话。适配Swift 6.2 / iOS 26+。

Contents

目录

Setup and Availability

配置与可用性

Project Configuration

项目配置

  1. Enable the HealthKit capability in Xcode (adds the entitlement)
  2. Add
    NSHealthShareUsageDescription
    (read) and
    NSHealthUpdateUsageDescription
    (write) to Info.plist
  3. For background delivery, enable the "Background Delivery" sub-capability
  1. 在Xcode中启用HealthKit功能(会添加权限配置)
  2. 在Info.plist中添加
    NSHealthShareUsageDescription
    (读取权限说明)和
    NSHealthUpdateUsageDescription
    (写入权限说明)
  3. 若使用后台推送,需启用“Background Delivery”子功能

Availability Check

可用性检查

Always check availability before accessing HealthKit. iPad and some devices do not support it.
swift
import HealthKit

let healthStore = HKHealthStore()

guard HKHealthStore.isHealthDataAvailable() else {
    // HealthKit not available on this device (e.g., iPad)
    return
}
Create a single
HKHealthStore
instance and reuse it throughout your app. It is thread-safe.
在访问HealthKit之前,务必先检查其可用性。iPad和部分设备不支持该功能。
swift
import HealthKit

let healthStore = HKHealthStore()

guard HKHealthStore.isHealthDataAvailable() else {
    // 此设备不支持HealthKit(例如iPad)
    return
}
创建单个
HKHealthStore
实例并在整个应用中复用,它是线程安全的。

Authorization

授权

Request only the types your app genuinely needs. App Review rejects apps that over-request.
swift
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
    )
}
仅请求应用实际需要的数据类型。请求过多权限的应用会被App Review拒绝。
swift
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
    )
}

Checking Authorization Status

检查授权状态

The app can only determine if it has not yet requested authorization. If the user denied access, HealthKit returns empty results rather than an error -- this is a privacy design.
swift
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
}
应用只能判断是否尚未请求授权。如果用户拒绝访问,HealthKit会返回空结果而非错误——这是隐私设计的要求。
swift
let status = healthStore.authorizationStatus(
    for: HKQuantityType(.stepCount)
)

switch status {
case .notDetermined:
    // 尚未请求授权——可以安全调用requestAuthorization
    break
case .sharingAuthorized:
    // 用户授予了写入权限
    break
case .sharingDenied:
    // 用户拒绝了写入权限(读取权限被拒绝无法与“无数据”区分)
    break
@unknown default:
    break
}

Reading Data: Sample Queries

读取数据:样本查询

Use
HKSampleQueryDescriptor
(async/await) for one-shot reads. Prefer descriptors over the older callback-based
HKSampleQuery
.
swift
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)")
}
使用
HKSampleQueryDescriptor
(async/await)执行一次性读取操作。优先使用描述符而非旧的基于回调的
HKSampleQuery
swift
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) 次/分,记录时间:\(sample.endDate)")
}

Reading Data: Statistics Queries

读取数据:统计查询

Use
HKStatisticsQueryDescriptor
for aggregated single-value stats (sum, average, min, max).
swift
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())
}
Options by data type:
  • Cumulative types (steps, calories):
    .cumulativeSum
  • Discrete types (heart rate, weight):
    .discreteAverage
    ,
    .discreteMin
    ,
    .discreteMax
使用
HKStatisticsQueryDescriptor
获取聚合的单值统计数据(总和、平均值、最小值、最大值)。
swift
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

Reading Data: Statistics Collection Queries

读取数据:统计集合查询

Use
HKStatisticsCollectionQueryDescriptor
for time-series data grouped into intervals -- ideal for charts.
swift
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
}
使用
HKStatisticsCollectionQueryDescriptor
获取按时间间隔分组的时间序列数据——非常适合用于图表展示。
swift
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
}

Long-Running Collection Query

长期运行的集合查询

Use
results(for:)
(plural) to get an
AsyncSequence
that emits updates as new data arrives:
swift
let updateStream = query.results(for: healthStore)

Task {
    for try await result in updateStream {
        // result.statisticsCollection contains updated data
    }
}
使用
results(for:)
(复数形式)获取
AsyncSequence
,当有新数据到达时会发出更新:
swift
let updateStream = query.results(for: healthStore)

Task {
    for try await result in updateStream {
        // result.statisticsCollection包含更新后的数据
    }
}

Writing Data

写入数据

Create
HKQuantitySample
objects and save them to the store.
swift
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)
}
Your app can only delete samples it created. Samples from other apps or Apple Watch are read-only.
创建
HKQuantitySample
对象并保存到存储中。
swift
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)
}
应用只能删除自己创建的样本。来自其他应用或Apple Watch的样本为只读。

Background Delivery

后台数据推送

Register for background updates so your app is launched when new data arrives. Requires the background delivery entitlement.
swift
func enableStepCountBackgroundDelivery() async throws {
    let stepType = HKQuantityType(.stepCount)

    try await healthStore.enableBackgroundDelivery(
        for: stepType,
        frequency: .hourly
    )
}
Pair with an
HKObserverQuery
to handle notifications. Always call the completion handler:
swift
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)
Frequencies:
.immediate
,
.hourly
,
.daily
,
.weekly
Call
enableBackgroundDelivery
once (e.g., at app launch). The system persists the registration.
注册后台更新,以便新数据到达时启动应用。需要后台推送权限。
swift
func enableStepCountBackgroundDelivery() async throws {
    let stepType = HKQuantityType(.stepCount)

    try await healthStore.enableBackgroundDelivery(
        for: stepType,
        frequency: .hourly
    )
}
搭配
HKObserverQuery
使用
来处理通知。必须调用完成处理程序:
swift
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
(例如在应用启动时),系统会保留该注册信息。

Workout Sessions

运动会话

Use
HKWorkoutSession
and
HKLiveWorkoutBuilder
to track live workouts. Available on watchOS 2+ and iOS 17+.
swift
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())
}

func endWorkout(
    session: HKWorkoutSession,
    builder: HKLiveWorkoutBuilder
) async throws {
    session.end()
    try await builder.endCollection(at: Date())
    try await builder.finishWorkout()
}
For full workout lifecycle management including pause/resume, delegate handling, and multi-device mirroring, see
references/healthkit-patterns.md
.
使用
HKWorkoutSession
HKLiveWorkoutBuilder
跟踪实时运动。支持watchOS 2+和iOS 17+。
swift
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())
}

func endWorkout(
    session: HKWorkoutSession,
    builder: HKLiveWorkoutBuilder
) async throws {
    session.end()
    try await builder.endCollection(at: Date())
    try await builder.finishWorkout()
}
关于完整的运动生命周期管理(包括暂停/恢复、委托处理和多设备镜像),请参阅
references/healthkit-patterns.md

Common Data Types

常见数据类型

HKQuantityTypeIdentifier

HKQuantityTypeIdentifier

IdentifierCategoryUnit
.stepCount
Fitness
.count()
.distanceWalkingRunning
Fitness
.meter()
.activeEnergyBurned
Fitness
.kilocalorie()
.basalEnergyBurned
Fitness
.kilocalorie()
.heartRate
Vitals
.count()/.minute()
.restingHeartRate
Vitals
.count()/.minute()
.oxygenSaturation
Vitals
.percent()
.bodyMass
Body
.gramUnit(with: .kilo)
.bodyMassIndex
Body
.count()
.height
Body
.meter()
.bodyFatPercentage
Body
.percent()
.bloodGlucose
Lab
.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
标识符分类单位
.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

HKCategoryTypeIdentifier

Common category types:
.sleepAnalysis
,
.mindfulSession
,
.appleStandHour
常见分类类型:
.sleepAnalysis
,
.mindfulSession
,
.appleStandHour

HKCharacteristicType

HKCharacteristicType

Read-only user characteristics:
.dateOfBirth
,
.biologicalSex
,
.bloodType
,
.fitzpatrickSkinType
只读用户特征:
.dateOfBirth
,
.biologicalSex
,
.bloodType
,
.fitzpatrickSkinType

HKUnit Reference

HKUnit 参考

swift
// 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
swift
// 基础单位
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())   // 心率(次/分)
HKUnit.meter().unitDivided(by: .second())   // 速度(米/秒)

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

Common Mistakes

常见错误

1. Over-requesting data types

1. 请求过多数据类型

DON'T -- request everything:
swift
// App Review will reject this
let allTypes: Set<HKObjectType> = [
    HKQuantityType(.stepCount),
    HKQuantityType(.heartRate),
    HKQuantityType(.bloodGlucose),
    HKQuantityType(.bodyMass),
    HKQuantityType(.oxygenSaturation),
    // ...20 more types the app never uses
]
DO -- request only what you use:
swift
let neededTypes: Set<HKObjectType> = [
    HKQuantityType(.stepCount),
    HKQuantityType(.activeEnergyBurned)
]
错误示例——请求所有类型:
swift
// 会被App Review拒绝
let allTypes: Set<HKObjectType> = [
    HKQuantityType(.stepCount),
    HKQuantityType(.heartRate),
    HKQuantityType(.bloodGlucose),
    HKQuantityType(.bodyMass),
    HKQuantityType(.oxygenSaturation),
    // ...还有20个应用从未使用的类型
]
正确做法——仅请求需要的类型:
swift
let neededTypes: Set<HKObjectType> = [
    HKQuantityType(.stepCount),
    HKQuantityType(.activeEnergyBurned)
]

2. Not handling authorization denial

2. 未处理授权被拒绝的情况

DON'T -- assume data will be returned:
swift
func getSteps() async throws -> Double {
    let result = try await query.result(for: healthStore)
    return result!.sumQuantity()!.doubleValue(for: .count()) // Crashes if denied
}
DO -- handle nil gracefully:
swift
func getSteps() async throws -> Double {
    let result = try await query.result(for: healthStore)
    return result?.sumQuantity()?.doubleValue(for: .count()) ?? 0
}
错误示例——假设一定会返回数据:
swift
func getSteps() async throws -> Double {
    let result = try await query.result(for: healthStore)
    return result!.sumQuantity()!.doubleValue(for: .count()) // 授权被拒绝时会崩溃
}
正确做法——优雅处理nil值:
swift
func getSteps() async throws -> Double {
    let result = try await query.result(for: healthStore)
    return result?.sumQuantity()?.doubleValue(for: .count()) ?? 0
}

3. Assuming HealthKit is always available

3. 假设HealthKit始终可用

DON'T -- skip the check:
swift
let store = HKHealthStore() // Crashes on iPad
try await store.requestAuthorization(toShare: types, read: types)
DO -- guard availability:
swift
guard HKHealthStore.isHealthDataAvailable() else {
    showUnsupportedDeviceMessage()
    return
}
错误示例——跳过可用性检查:
swift
let store = HKHealthStore() // 在iPad上会崩溃
try await store.requestAuthorization(toShare: types, read: types)
正确做法——先检查可用性:
swift
guard HKHealthStore.isHealthDataAvailable() else {
    showUnsupportedDeviceMessage()
    return
}

4. Running heavy queries on the main thread

4. 在主线程运行重型查询

DON'T -- use old callback-based queries on main thread. DO -- use async descriptors:
swift
// Bad: HKSampleQuery with callback on main thread
// Good: async descriptor
func loadAllData() async throws -> [HKQuantitySample] {
    let descriptor = HKSampleQueryDescriptor(
        predicates: [.quantitySample(type: stepType)],
        sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)],
        limit: 100
    )
    return try await descriptor.result(for: healthStore)
}
错误示例——在主线程使用旧的基于回调的查询。正确做法——使用异步描述符:
swift
// 错误:在主线程使用HKSampleQuery回调
// 正确:使用异步描述符
func loadAllData() async throws -> [HKQuantitySample] {
    let descriptor = HKSampleQueryDescriptor(
        predicates: [.quantitySample(type: stepType)],
        sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)],
        limit: 100
    )
    return try await descriptor.result(for: healthStore)
}

5. Forgetting to call completionHandler in observer queries

5. 在观察者查询中忘记调用completionHandler

DON'T -- skip the completion handler:
swift
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
    processNewData()
    // Forgot to call handler() -- system won't schedule next delivery
}
DO -- always call it:
swift
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
    defer { handler() }
    processNewData()
}
错误示例——跳过完成处理程序:
swift
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
    processNewData()
    // 忘记调用handler()——系统不会安排下一次推送
}
正确做法——始终调用:
swift
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
    defer { handler() }
    processNewData()
}

6. Using wrong statistics options for the data type

6. 为数据类型使用错误的统计选项

DON'T -- use cumulative sum on discrete types:
swift
// Heart rate is discrete, not cumulative -- this returns nil
let query = HKStatisticsQueryDescriptor(
    predicate: heartRatePredicate,
    options: .cumulativeSum
)
DO -- match options to data type:
swift
// Use discrete options for discrete types
let query = HKStatisticsQueryDescriptor(
    predicate: heartRatePredicate,
    options: .discreteAverage
)
错误示例——对离散类型使用累积总和:
swift
// 心率是离散类型,不是累积类型——此查询会返回nil
let query = HKStatisticsQueryDescriptor(
    predicate: heartRatePredicate,
    options: .cumulativeSum
)
正确做法——为数据类型匹配对应的选项:
swift
// 对离散类型使用离散选项
let query = HKStatisticsQueryDescriptor(
    predicate: heartRatePredicate,
    options: .discreteAverage
)

Review Checklist

审核检查清单

  • HKHealthStore.isHealthDataAvailable()
    checked before any HealthKit access
  • Only necessary data types requested in authorization
  • Info.plist
    includes
    NSHealthShareUsageDescription
    and/or
    NSHealthUpdateUsageDescription
  • HealthKit capability enabled in Xcode project
  • Authorization denial handled gracefully (nil results, not crashes)
  • Single
    HKHealthStore
    instance reused (not created per query)
  • Async query descriptors used instead of callback-based queries
  • Heavy queries not blocking main thread
  • Statistics options match data type (cumulative vs. discrete)
  • Background delivery paired with
    HKObserverQuery
    and
    completionHandler
    called
  • Background delivery entitlement enabled if using
    enableBackgroundDelivery
  • Workout sessions properly ended and builder finalized
  • Write operations only for sample types the app created
  • 在访问任何HealthKit功能前,已检查
    HKHealthStore.isHealthDataAvailable()
  • 授权时仅请求必要的数据类型
  • Info.plist中包含
    NSHealthShareUsageDescription
    和/或
    NSHealthUpdateUsageDescription
  • Xcode项目中已启用HealthKit功能
  • 已优雅处理授权被拒绝的情况(返回nil结果而非崩溃)
  • 复用单个
    HKHealthStore
    实例(而非每次查询都创建新实例)
  • 使用异步查询描述符而非基于回调的查询
  • 重型查询未阻塞主线程
  • 统计选项与数据类型匹配(累积型 vs 离散型)
  • 后台推送已搭配
    HKObserverQuery
    使用,且已调用
    completionHandler
  • 若使用
    enableBackgroundDelivery
    ,已启用后台推送权限
  • 运动会话已正确结束,且builder已完成最终处理
  • 仅对应用创建的样本类型执行写入操作

References

参考资料