Getting started with HealthKit
Health & Fitness 入门 19m

HealthKit 入门指南

Getting started with HealthKit

2020年6月23日

在 Apple 官方观看视频

一句话判断

这是一场 HealthKit 的「从零到一」入门 Session——从权限请求到数据读写再到后台交付,把 HealthKit 的核心工作流用最清晰的方式串了一遍,如果你之前没碰过 HealthKit,看完这场就能动手了。

这场 Session 讲了什么

这场 Session 是 HealthKit 的入门教程,目标受众是完全没有 HealthKit 经验的开发者。内容涵盖了 HealthKit 的核心概念、数据模型、权限系统和基本的数据读写操作。

核心概念部分:HealthKit 是 Apple 设备上的健康数据中枢,所有健康相关数据(步数、心率、睡眠、血压等)都存储在 HealthKit 中。Apple Health app 是用户界面,HealthKit 是开发者的 API 层。

数据模型部分:HKObject 是所有健康数据的基类,HKQuantitySample 存储数值型数据(步数、心率),HKCategorySample 存储分类数据(睡眠分析),HKCorrelation 存储关联数据(一顿饭的食物+血压)。每个样本都有开始时间、结束时间、来源(哪个 app 写入的)。

权限系统:HealthKit 使用严格的权限模型,app 必须在 Info.plist 中声明需要读写的每种数据类型,用户逐项授权。隐私描述文字需要具体说明用途,不能写「需要健康数据」这种笼统的话。

值得深挖的点

后台交付(Background Delivery)的机制

HKObserverQuery 允许你的 app 在后台被唤醒,当指定类型的健康数据有新数据时。Apple Watch 采集的心率、步数等数据通过后台交付推送到 iOS app。后台唤醒的时间有限(大约 30 秒),你不能做复杂的计算,只能做简单的数据提取和本地通知。

HKSampleQuery 与 HKStatisticsQuery 的选择

HKSampleQuery 返回原始的样本数据(每条心率记录、每段步数记录)。HKStatisticsQuery 返回聚合统计(每天的平均心率、每周的总步数)。大多数健康类 app 应该优先用统计查询,除非你需要显示每条原始数据的图表。

代码片段

import HealthKit

// 第一步:检查设备是否支持 HealthKit
class HealthManager {
    let healthStore = HKHealthStore()
    
    func setup() {
        guard HKHealthStore.isHealthDataAvailable() else {
            print("此设备不支持 HealthKit")
            return
        }
        requestAuthorization()
    }
    
    // 第二步:请求权限
    func requestAuthorization() {
        // 要读取的数据类型
        let typesToRead: Set<HKObjectType> = [
            HKObjectType.quantityType(forIdentifier: .stepCount)!,
            HKObjectType.quantityType(forIdentifier: .heartRate)!,
            HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
        ]
        
        // 要写入的数据类型(通常是 app 自己产生的运动数据)
        let typesToWrite: Set<HKSampleType> = [
            HKObjectType.quantityType(forIdentifier: .stepCount)!
        ]
        
        healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { success, error in
            if success {
                print("HealthKit 权限已授权")
                self.queryTodaySteps()
            }
        }
    }
    
    // 第三步:查询今日步数
    func queryTodaySteps() {
        let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
        let now = Date()
        let startOfDay = Calendar.current.startOfDay(for: now)
        let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
        
        let query = HKStatisticsQuery(
            quantityType: stepType,
            quantitySamplePredicate: predicate,
            options: .cumulativeSum
        ) { _, result, error in
            guard let result = result, let sum = result.sumQuantity() else { return }
            let steps = sum.doubleValue(for: HKUnit.count())
            print("今日步数: \(Int(steps))")
        }
        
        healthStore.execute(query)
    }
}
// 写入运动数据到 HealthKit
func saveWorkout(distance: Double, duration: TimeInterval) {
    let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
    let distanceQuantity = HKQuantity(unit: HKUnit.meter(), doubleValue: distance)
    
    let sample = HKQuantitySample(
        type: distanceType,
        quantity: distanceQuantity,
        start: Date().addingTimeInterval(-duration),
        end: Date()
    )
    
    healthStore.save(sample) { success, error in
        if success {
            print("运动数据已保存")
        }
    }
}

// 注册后台数据交付
func observeHeartRate() {
    let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
    
    let query = HKObserverQuery(sampleType: heartRateType, predicate: nil) { [weak self] _, completionHandler, error in
        // 后台被唤醒,查询最新心率
        self?.fetchLatestHeartRate()
        
        // 必须调用 completionHandler
        completionHandler()
    }
    
    healthStore.execute(query)
    
    // 启用后台交付
    healthStore.enableBackgroundDelivery(for: heartRateType, frequency: .immediate) { success, error in
        if success {
            print("心率后台交付已启用")
        }
    }
}

最佳实践

  1. 权限请求要分散到功能点。不要在 app 启动时一口气请求所有健康数据权限。用户在打开「心率」页面时才请求心率权限,打开「睡眠」页面时才请求睡眠权限。这样用户能理解每个权限的用途。
  2. Info.plist 的隐私描述要具体NSHealthShareUsageDescription 不要写「需要读取健康数据」,要写「读取步数和心率来显示您的每日运动报告」。苹果会审核这段文字。
  3. 处理权限被拒绝的 UI。用户可能拒绝部分权限,你的 app 要能优雅降级。步数权限被拒绝了?显示「请在健康 app 中授权步数访问」的提示,而不是空白页面。

还有什么值得关注

  • HealthKit 数据只在用户设备上,苹果看不到也不存储在云端(除非用户开启了 iCloud 健康同步)。
  • 模拟器不支持 HealthKit,开发和测试必须用真机。
  • iOS 14 新增了 Mobility 数据类型(详见 Session 10656),如果你做健康类 app 值得关注。
WWDC 2020