What's new in HealthKit
Health & Fitness 进阶 22m

HealthKit 新特性概览

What's new in HealthKit

2020年6月25日

在 Apple 官方观看视频

一句话判断

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 中的原始数据
WWDC 2020