HealthKit 新特性概览
What's new in HealthKit
2020年6月25日
一句话判断
HealthKit 在 iOS 14 的新能力——特别是 Mobility 健康数据和递减采样——让你的健康应用可以做到比以前精细得多的数据分析。
这场 Session 讲了什么
HealthKit 在 iOS 14 和 watchOS 7 中获得了多项重要更新。Session 首先介绍了新增的健康数据类型,其中最重要的是 Mobility(活动能力)类指标。这是一组衡量用户行走稳定性和步态健康的数据,包括步行速度、步长、支撑时间、步行不对称性和双支撑时间百分比。这些数据由 Apple Watch 自动采集,为健康和康复类应用提供了全新的数据维度。
另一个重大更新是递减采样(Descending Samples)查询支持。以前 HealthKit 的 HKSampleQuery 只能按时间正序返回结果,要获取”最新的 N 条数据”需要先查询全部再排序。iOS 14 的 HKSampleQueryDescriptor(Swift 的新 API 风格)支持直接设置 sort order 为降序,配合 limit 使用,高效获取最近的数据。
Session 还介绍了 HealthKit 的新同步框架——HKHealthStore 现在支持通过 HKObserverQuery 更细粒度地监听数据变化,以及新的批量操作 API 来高效导入大量健康数据。
值得深挖的点
Mobility 数据的实际应用场景
Mobility 指标的核心价值在于趋势分析。单次步态数据的绝对值意义有限,但连续几周/几个月的趋势变化可以揭示用户健康状态的显著转变。Session 展示了几个应用场景:老年用户的跌倒风险评估(步行速度和双支撑时间的变化趋势)、术后康复追踪(步行不对称性的改善曲线)、以及运动表现分析(步长和支撑时间的优化)。
新的 QueryDescriptor API
iOS 14 引入了 Swift-native 的查询 API——HKSampleQueryDescriptor。与传统的 HKSampleQuery 相比,新 API 使用泛型和结果构建器模式,提供了类型安全和可组合的查询构建体验。更重要的是,它天然支持 async/await 风格的异步调用,不需要嵌套 completion handler。
代码片段
import HealthKit
// 请求 Mobility 数据的读取权限
class MobilityManager {
let healthStore = HKHealthStore()
func requestAuthorization() {
guard HKHealthStore.isHealthDataAvailable() else { return }
// Mobility 相关的数据类型
let typesToRead: Set<HKObjectType> = [
HKObjectType.quantityType(forIdentifier: .walkingSpeed)!,
HKObjectType.quantityType(forIdentifier: .walkingStepLength)!,
HKObjectType.quantityType(forIdentifier: .walkingAsymmetryPercentage)!,
HKObjectType.quantityType(forIdentifier: .walkingDoubleSupportPercentage)!,
HKObjectType.quantityType(forIdentifier: .stepCount)!,
]
healthStore.requestAuthorization(toShare: nil, read: typesToRead) { success, error in
if success {
print("HealthKit 授权成功")
self.fetchMobilityData()
}
}
}
func fetchMobilityData() {
// 查询最近 7 天的步行速度
let walkingSpeedType = HKQuantityType.quantityType(forIdentifier: .walkingSpeed)!
let predicate = HKQuery.predicateForSamples(
withStart: Date().addingTimeInterval(-7 * 24 * 3600),
end: Date(),
options: .strictStartDate
)
let query = HKSampleQuery(
sampleType: walkingSpeedType,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [NSSortDescriptor(
key: HKSampleSortIdentifierStartDate,
ascending: false // 降序:最新的数据在前
)]
) { _, samples, error in
guard let samples = samples as? [HKQuantitySample] else { return }
for sample in samples {
let speed = sample.quantity.doubleValue(
for: HKUnit.meter().unitDivided(by: .second())
)
print("步行速度: \(String(format: "%.2f", speed)) m/s")
}
}
healthStore.execute(query)
}
}
// 使用新的 HKSampleQueryDescriptor(iOS 14+)
import HealthKit
@available(iOS 14, *)
class ModernHealthQuery {
let healthStore = HKHealthStore()
func fetchRecentStepCounts() async throws -> [HKQuantitySample] {
let stepType = HKQuantityType(.stepCount)
// 使用 Descriptor 风格构建查询
let descriptor = HKSampleQueryDescriptor(
predicates: [
.sampleType(
stepType,
predicate: HKQuery.predicateForSamples(
withStart: Date().addingTimeInterval(-30 * 24 * 3600),
end: Date(),
options: .strictStartDate
)
)
],
sortDescriptors: [
SortDescriptor(\HKSample.startDate, order: .reverse)
],
limit: 30 // 最近 30 条
)
let results = try await descriptor.results(for: healthStore)
return Array(results)
}
}
// 监听健康数据变化
class HealthDataObserver {
let healthStore = HKHealthStore()
func observeWalkingSpeedUpdates() {
let speedType = HKQuantityType.quantityType(forIdentifier: .walkingSpeed)!
// 创建观察者查询
let query = HKObserverQuery(sampleType: speedType, predicate: nil) { [weak self] _, completionHandler, error in
if let error = error {
print("观察错误: \(error)")
return
}
// 有新的步行速度数据,获取最新值
self?.fetchLatestWalkingSpeed()
// 必须调用 completionHandler
completionHandler()
}
healthStore.execute(query)
// 启用后台更新(需要在 Info.plist 配置 background modes)
healthStore.enableBackgroundDelivery(
for: speedType,
frequency: .immediate
) { success, error in
print("后台更新已\(success ? "启用" : "失败")")
}
}
func fetchLatestWalkingSpeed() {
// 获取最新一条步行速度数据
let speedType = HKQuantityType.quantityType(forIdentifier: .walkingSpeed)!
let query = HKSampleQuery(
sampleType: speedType,
predicate: nil,
limit: 1,
sortDescriptors: [NSSortDescriptor(
key: HKSampleSortIdentifierStartDate, ascending: false
)]
) { _, samples, _ in
guard let latest = samples?.first as? HKQuantitySample else { return }
let speed = latest.quantity.doubleValue(
for: HKUnit.meter().unitDivided(by: .second())
)
print("最新步行速度: \(String(format: "%.2f", speed)) m/s")
}
healthStore.execute(query)
}
}
最佳实践
- 在请求 HealthKit 授权时只申请你实际需要的数据类型,不要”一把抓”
- Mobility 数据的变化趋势比绝对值更有意义,UI 上应该重点展示趋势图而非单次数值
- 使用
HKObserverQuery监听数据变化时,务必在回调中调用completionHandler(),否则系统不会继续发送更新 - 批量查询健康数据时使用 limit 参数,避免一次性加载过多数据导致内存问题
- 在 UI 中清晰说明数据来源(Apple Watch 自动采集 vs 用户手动输入),帮助用户理解数据可信度
还有什么值得关注
- watchOS 7 新增了对睡眠阶段的自动追踪(Core Motion),数据通过 HealthKit 提供
- 新的 HealthKit 单位类型包括
HKUnit.meter().unitDivided(by: .second())(步行速度)等 - Session 提到 HealthKit 数据在 iCloud 中同步时是端到端加密的,开发者无法直接访问 iCloud 中的原始数据