healthkit by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill healthkit从 Apple Health 健康数据存储中读取和写入健康与健身数据。涵盖授权、查询、写入样本、后台交付和锻炼会话。目标版本为 Swift 6.2 / iOS 26+。
NSHealthShareUsageDescription(读取)和 NSHealthUpdateUsageDescription(写入)在访问 HealthKit 之前,务必检查其可用性。iPad 和某些设备不支持它。
import HealthKit
let healthStore = HKHealthStore()
guard HKHealthStore.isHealthDataAvailable() else {
// 此设备不支持 HealthKit(例如 iPad)
return
}
创建单个 HKHealthStore 实例并在整个应用中重复使用。它是线程安全的。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
仅请求您的应用真正需要的类型。应用审核会拒绝请求过多的应用。
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
)
}
应用只能确定是否尚未请求授权。如果用户拒绝访问,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)
}
您的应用只能删除它自己创建的样本。来自其他应用或 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 一次(例如在应用启动时)。系统会持久化该注册。
使用 HKWorkoutSession 和 HKLiveWorkoutBuilder 来跟踪实时锻炼。适用于 watchOS 2+ 和 iOS 17+。
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。
| 标识符 | 类别 | 单位 |
|---|---|---|
.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)) |
常见类别类型:.sleepAnalysis、.mindfulSession、.appleStandHour
只读用户特征:.dateOfBirth、.biologicalSex、.bloodType、.fitzpatrickSkinType
// 基本单位
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) // 分升
不要——请求所有内容:
// 应用审核会拒绝此请求
let allTypes: Set<HKObjectType> = [
HKQuantityType(.stepCount),
HKQuantityType(.heartRate),
HKQuantityType(.bloodGlucose),
HKQuantityType(.bodyMass),
HKQuantityType(.oxygenSaturation),
// ... 应用从未使用的其他 20 种类型
]
要——仅请求您使用的内容:
let neededTypes: Set<HKObjectType> = [
HKQuantityType(.stepCount),
HKQuantityType(.activeEnergyBurned)
]
不要——假设数据会被返回:
func getSteps() async throws -> Double {
let result = try await query.result(for: healthStore)
return result!.sumQuantity()!.doubleValue(for: .count()) // 如果被拒绝,会崩溃
}
要——优雅地处理 nil:
func getSteps() async throws -> Double {
let result = try await query.result(for: healthStore)
return result?.sumQuantity()?.doubleValue(for: .count()) ?? 0
}
不要——跳过检查:
let store = HKHealthStore() // 在 iPad 上会崩溃
try await store.requestAuthorization(toShare: types, read: types)
要——检查可用性:
guard HKHealthStore.isHealthDataAvailable() else {
showUnsupportedDeviceMessage()
return
}
不要——在主线程上使用旧的基于回调的查询。要——使用异步描述符:
// 错误:在主线程上使用带回调的 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)
}
不要——跳过完成处理程序:
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
processNewData()
// 忘记调用 handler() —— 系统不会安排下一次交付
}
要——始终调用它:
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
defer { handler() }
processNewData()
}
不要——在离散类型上使用累积总和:
// 心率是离散的,不是累积的——这会返回 nil
let query = HKStatisticsQueryDescriptor(
predicate: heartRatePredicate,
options: .cumulativeSum
)
要——使选项与数据类型匹配:
// 对离散类型使用离散选项
let query = HKStatisticsQueryDescriptor(
predicate: heartRatePredicate,
options: .discreteAverage
)
HKHealthStore.isHealthDataAvailable()Info.plist 包含了 NSHealthShareUsageDescription 和/或 NSHealthUpdateUsageDescriptionHKHealthStore 实例(非每次查询都创建)HKObserverQuery 配对使用,并且调用了 completionHandlerenableBackgroundDelivery,则启用了后台交付授权references/healthkit-patterns.md每周安装量
341
代码仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex338
cursor335
gemini-cli334
amp334
cline334
github-copilot334
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+.
NSHealthShareUsageDescription (read) and NSHealthUpdateUsageDescription (write) to Info.plistAlways check availability before accessing HealthKit. iPad and some devices do not support it.
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.
Request only the types your app genuinely needs. App Review rejects apps that over-request.
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
)
}
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.
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
}
Use HKSampleQueryDescriptor (async/await) for one-shot reads. Prefer descriptors over the older callback-based 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)")
}
Use HKStatisticsQueryDescriptor for aggregated single-value stats (sum, average, min, max).
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:
.cumulativeSum.discreteAverage, .discreteMin, .discreteMaxUse HKStatisticsCollectionQueryDescriptor for time-series data grouped into intervals -- ideal for charts.
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
}
Use results(for:) (plural) to get an AsyncSequence that emits updates as new data arrives:
let updateStream = query.results(for: healthStore)
Task {
for try await result in updateStream {
// result.statisticsCollection contains updated data
}
}
Create HKQuantitySample objects and save them to the store.
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.
Register for background updates so your app is launched when new data arrives. Requires the background delivery entitlement.
func enableStepCountBackgroundDelivery() async throws {
let stepType = HKQuantityType(.stepCount)
try await healthStore.enableBackgroundDelivery(
for: stepType,
frequency: .hourly
)
}
Pair with anHKObserverQuery to handle notifications. Always call the completion handler:
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.
Use HKWorkoutSession and HKLiveWorkoutBuilder to track live workouts. Available on watchOS 2+ and iOS 17+.
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.
| Identifier | Category | Unit |
|---|---|---|
.stepCount | Fitness | .count() |
.distanceWalkingRunning | Fitness | .meter() |
.activeEnergyBurned | Fitness | .kilocalorie() |
.basalEnergyBurned |
Common category types: .sleepAnalysis, .mindfulSession, .appleStandHour
Read-only user characteristics: .dateOfBirth, .biologicalSex, .bloodType, .fitzpatrickSkinType
// 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
DON'T -- request everything:
// 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:
let neededTypes: Set<HKObjectType> = [
HKQuantityType(.stepCount),
HKQuantityType(.activeEnergyBurned)
]
DON'T -- assume data will be returned:
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:
func getSteps() async throws -> Double {
let result = try await query.result(for: healthStore)
return result?.sumQuantity()?.doubleValue(for: .count()) ?? 0
}
DON'T -- skip the check:
let store = HKHealthStore() // Crashes on iPad
try await store.requestAuthorization(toShare: types, read: types)
DO -- guard availability:
guard HKHealthStore.isHealthDataAvailable() else {
showUnsupportedDeviceMessage()
return
}
DON'T -- use old callback-based queries on main thread. DO -- use async descriptors:
// 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)
}
DON'T -- skip the completion handler:
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
processNewData()
// Forgot to call handler() -- system won't schedule next delivery
}
DO -- always call it:
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, handler, _ in
defer { handler() }
processNewData()
}
DON'T -- use cumulative sum on discrete types:
// Heart rate is discrete, not cumulative -- this returns nil
let query = HKStatisticsQueryDescriptor(
predicate: heartRatePredicate,
options: .cumulativeSum
)
DO -- match options to data type:
// Use discrete options for discrete types
let query = HKStatisticsQueryDescriptor(
predicate: heartRatePredicate,
options: .discreteAverage
)
HKHealthStore.isHealthDataAvailable() checked before any HealthKit accessInfo.plist includes NSHealthShareUsageDescription and/or NSHealthUpdateUsageDescriptionHKHealthStore instance reused (not created per query)HKObserverQuery and completionHandler calledenableBackgroundDeliveryreferences/healthkit-patterns.mdWeekly Installs
341
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex338
cursor335
gemini-cli334
amp334
cline334
github-copilot334
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
| 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)) |