关于 healthkit
面向 Swift 开发者的 HealthKit 技能,介绍 Apple Health 数据的授权、读取、写入、后台更新和运动会话。内容聚焦现代异步查询及实现注意事项。
主要功能
- 01检查 HealthKit 可用性并请求授权
- 02读取样本及统计数据
- 03写入健康与健身样本
- 04设置后台数据传递
- 05管理实时运动会话
- 06核对数据类型与单位
适用场景
- 01读取步数、心率等健康数据
- 02按时间区间汇总数据并绘图
- 03保存应用产生的健康样本
- 04新健康数据到达时更新应用
- 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+。
目录
设置与可用性
项目配置
- 在 Xcode 中启用 HealthKit 功能(添加权利配置)
- 在 Info.plist 中添加
NSHealthShareUsageDescription(读取)和NSHealthUpdateUsageDescription(写入) - 如需后台交付,请启用“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
常见错误
- 请求过多数据类型。 只请求功能实际使用的读写类型;范围过广的 HealthKit 权限弹窗可能带来 App Review 风险。
- 将读取授权视同写入授权。 保存前可以检查
.sharingAuthorized,但读取权限被拒绝会受到隐私保护,结果看起来会像仅包含应用自身数据、空结果或部分结果。 - 跳过
isHealthDataAvailable()。 访问 HealthKit 前先检查,并在数据存储不可用或受限时妥善处理,避免崩溃。 - 在新的异步代码中使用回调查询。 单次读取和统计优先使用异步描述符,并避免在主线程执行范围过广的查询。
- 忘记调用观察者完成处理程序。 务必调用该处理程序;遗漏完成调用可能会延迟或停止后续后台传递。
- 以为
.immediate意味着立即传递。 后台传递受系统限制,必须在设备上测试。 - 对离散值使用累积统计。 根据数据类型选择统计选项:步数/能量使用累积总和,心率、体重及类似样本使用离散平均值/最小值/最大值。
审核清单
- 在访问 HealthKit 前检查
HKHealthStore.isHealthDataAvailable() - 授权时仅请求必要的数据类型
-
Info.plist包含NSHealthShareUsageDescription和/或NSHealthUpdateUsageDescription - 在 Xcode 项目中启用 HealthKit 功能
- 保存前检查写入授权;将读取权限被拒绝的情况作为部分或空查询结果处理
- 重用单个
HKHealthStore实例(而非每次查询都创建一个) - 使用异步查询描述符,而非基于回调的查询
- 避免在主线程执行耗时查询
- 统计选项与数据类型匹配(累积或离散)
- 将后台传递与应用启动时设置的
HKObserverQuery配合使用,并调用completionHandler - 使用
enableBackgroundDelivery时启用后台传递权限 - 在设备上测试后台传递,并考虑频率限制
- 停止锻炼时,等待委托的
.stopped状态转换后再调用endCollection和finishWorkout;仅在成功完成最终处理后清除状态 - 处理锻炼 API 可用性和实时心率传感器要求
- 删除操作仅针对应用此前保存的对象