WWDC Quick Look 💓 By SwiftGGTeam
Measure health with motion

Measure health with motion

观看原视频

Highlight

Apple 在 iOS 15 和 watchOS 中把行走耐力、行走稳定性和跌倒风险暴露给 HealthKit,开发者可以用 Six-Minute Walk 重新校准和 Walking Steadiness 构建远程康复、物理治疗和健康监测功能。

核心内容

Jamie 刚做完膝盖手术。医生知道她术后几乎走不动,但 Apple Watch 的 Six-Minute Walk 估算只出现了小幅下降。问题出在数据窗口:这个估算会使用一段历史活动和运动数据,窗口里还包含术前状态,术后的真实变化被稀释了。

02:13)iOS 15 提供了 HealthKit estimate recalibration API(估算重新校准 API)。应用知道手术日期后,可以请求从这个日期之后重新建立 Six-Minute Walk 估算。这样,术后几次估算会更快反映耐力下降,护理团队可以更早判断恢复是否顺利。

行走能力还包含另一个问题:患者能走多远,只说明耐力;患者走得稳不稳,关系到跌倒风险。远程物理治疗里,医生很难每周看见患者的真实步态。患者可能按时打卡训练,但稳定性正在下降。

06:24)Apple Walking Steadiness(行走稳定性)用 iPhone 测量行走质量。HealthKit 每周保存一次分数,并把分数归类为 OK、Low、Very Low。用户在健康 App 中开启后,稳定性降到 Low 或 Very Low 时,系统会写入通知事件。应用可以监听这些事件,提示患者安排复诊。

这两个能力的共同点很明确:把诊室里的阶段性评估,变成日常生活里的连续信号。开发者不需要额外硬件,也不需要让用户每天完成专门测试;前提是用户授权 HealthKit,并携带 iPhone 或 Apple Watch 产生足够的样本。

详细内容

1. Six-Minute Walk:先拿到读写授权

03:39)重新校准通过 HKHealthStore.recalibrateEstimates 完成。Apple 在演讲中说明了三个要求:样本类型必须支持重新校准,应用需要新的 entitlement(授权能力),还需要同时获得读取和写入该类型的 HealthKit 授权。iOS 15 中可重新校准的类型是 sixMinuteWalkTestDistance

import HealthKit

let healthStore = HKHealthStore()
let types: Set = [
    HKObjectType.quantityType(forIdentifier: .sixMinuteWalkTestDistance)!
]

healthStore.requestAuthorization(toShare: types, read: types) { success, error in
    if let error = error {
        print(error.localizedDescription)
        return
    }

    print("Six-Minute Walk authorization: \(success)")
}

关键点:

  • import HealthKit:使用 HealthKit 类型和授权 API。
  • HKHealthStore():创建访问 HealthKit 数据库的入口。
  • HKObjectType.quantityType(forIdentifier: .sixMinuteWalkTestDistance)!:取出 Six-Minute Walk 距离对应的数量类型。
  • toShare: types:请求写入权限,重新校准需要这个权限。
  • read: types:请求读取权限,应用需要读取估算结果。
  • success:表示用户是否完成授权流程。
  • error:处理授权请求失败。

2. Six-Minute Walk:用手术日期重新校准估算

04:27)应用知道 Jamie 的手术日期后,调用 recalibrateEstimates(sampleType:date:)。系统会弹出提示,让用户确认这次操作会影响健康数据。重新校准不会改写 HealthKit 中已经存在的估算;它影响之后生成的估算。演讲还说明,重新校准后可能需要最多 14 天重新积累足够活动历史,效果也会随着日期变远而逐渐结束。

import HealthKit

let healthStore = HKHealthStore()
let sixMinuteWalkType = HKSampleType.quantityType(forIdentifier: .sixMinuteWalkTestDistance)!
let surgeryDate = Date()

if sixMinuteWalkType.allowsRecalibrationForEstimates {
    healthStore.recalibrateEstimates(sampleType: sixMinuteWalkType, date: surgeryDate) {
        success, error in

        if let error = error {
            print(error.localizedDescription)
            return
        }

        print("Recalibration requested: \(success)")
    }
}

关键点:

  • HKSampleType.quantityType(forIdentifier:):取得可传给重新校准方法的样本类型。
  • surgeryDate:代表急性事件发生日期,例如手术日期或受伤日期。
  • allowsRecalibrationForEstimates:检查该样本类型是否支持估算重新校准。
  • recalibrateEstimates(sampleType:date:):要求 HealthKit 从指定日期之后重新建立估算。
  • success:表示请求是否成功提交。
  • error:处理 entitlement、授权或系统调用失败。

3. Walking Steadiness:读取最新分数

11:22)Walking Steadiness 分数是 HealthKit quantity type(数量类型),名称为 appleWalkingSteadiness。它是 Apple 定义的只读指标,单位是 percent,范围从 0 到 1。分数每周写入一次;数据不足时,估算可能延迟。

import HealthKit

let healthKitStore = HKHealthStore()
let types: Set = [
    HKObjectType.quantityType(forIdentifier: .walkingSteadiness)!
]

healthKitStore.requestAuthorization(toShare: nil, read: types) { success, error in
    if let error = error {
        print(error.localizedDescription)
        return
    }

    print("Walking Steadiness authorization: \(success)")
}

关键点:

  • HKHealthStore():创建 HealthKit 访问入口。
  • .walkingSteadiness:取出行走稳定性分数类型。
  • toShare: nil:该指标只读,应用不写入。
  • read: types:请求读取 Walking Steadiness 分数。
  • success:记录授权结果。
  • error:处理授权错误。

拿到授权后,查询最新一条分数。

import HealthKit

let healthStore = HKHealthStore()
let steadinessType = HKObjectType.quantityType(forIdentifier: .walkingSteadiness)!
let sortByEndDate = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)

let query = HKSampleQuery(sampleType: steadinessType,
                          predicate: nil,
                          limit: 1,
                          sortDescriptors: [sortByEndDate]) { query, samples, error in

    if let error = error {
        print(error.localizedDescription)
        return
    }

    if let sample = samples?.first as? HKQuantitySample {
        let recentScore = sample.quantity.doubleValue(for: .percent())
        updateStatus(score: recentScore)
    }
}

healthStore.execute(query)

关键点:

  • steadinessType:HealthKit 中的 Walking Steadiness 数量类型。
  • sortByEndDate:按样本结束时间倒序排序,让最新样本排在前面。
  • HKSampleQuery:从 HealthKit 读取样本。
  • predicate: nil:不限制时间范围,直接查全量样本。
  • limit: 1:只要最新一条。
  • samples?.first as? HKQuantitySample:把返回样本转换为数量样本。
  • .percent():以百分比单位读取分数。
  • updateStatus(score:):把分数显示到应用界面或状态模型。

4. Walking Steadiness:把分数转成分类

12:21)单独的数值不容易解释。HealthKit 提供 HKAppleWalkingSteadinessClassification 枚举,把分数转换为 .ok.low.veryLow。演讲中的场景是物理治疗应用:患者是 Low 时,应用可以提示护理团队关注跌倒风险。

import HealthKit

let healthStore = HKHealthStore()
let steadinessType = HKObjectType.quantityType(forIdentifier: .walkingSteadiness)!
let sortByEndDate = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)

let query = HKSampleQuery(sampleType: steadinessType,
                          predicate: nil,
                          limit: 1,
                          sortDescriptors: [sortByEndDate]) { query, samples, error in

    if let error = error {
        print(error.localizedDescription)
        return
    }

    if let sample = samples?.first as? HKQuantitySample {
        let recentScore = sample.quantity.doubleValue(for: .percent())
        let recentClassification = HKAppleWalkingSteadinessClassification(for: sample.quantity)

        updateStatus(classification: recentClassification, score: recentScore)
    }
}

healthStore.execute(query)

关键点:

  • HKSampleQuery:复用读取最新分数的查询。
  • recentScore:保留原始分数,适合画趋势图。
  • HKAppleWalkingSteadinessClassification(for:):用 HealthKit API 将样本数值转换成分类。
  • recentClassification:得到 OK、Low 或 Very Low。
  • updateStatus(classification:score:):同时展示分类和分数,方便患者理解。

5. Walking Steadiness Event:监听系统通知事件

13:15)Walking Steadiness 通知会作为 HealthKit category type(分类类型)样本写入,名称是 appleWalkingSteadinessEvent。事件有四类:.initialLow.initialVeryLow.repeatLow.repeatVeryLow。前两类在用户进入 Low 或 Very Low 大约一个月后触发;后两类在用户持续处于同一分类时大约每三个月提醒一次。

import HealthKit

let healthKitStore = HKHealthStore()
let types: Set = [
    HKObjectType.categoryType(forIdentifier: .walkingSteadinessEvent)!
]

healthKitStore.requestAuthorization(toShare: nil, read: types) { success, error in
    if let error = error {
        print(error.localizedDescription)
        return
    }

    print("Walking Steadiness Event authorization: \(success)")
}

关键点:

  • categoryType(forIdentifier:):读取 Walking Steadiness 事件使用分类类型。
  • .walkingSteadinessEvent:对应系统写入的稳定性通知事件。
  • toShare: nil:事件由系统写入,应用只读取。
  • read: types:请求读取事件样本。
  • success:确认用户是否授权读取。

授权后,用 HKObserverQuery 监听新事件。

import HealthKit

let healthStore = HKHealthStore()
let notificationType = HKCategoryType.categoryType(forIdentifier: .appleWalkingSteadinessEvent)!

let query = HKObserverQuery(sampleType: notificationType, predicate: nil) {
    query, completionHandler, errorOrNil in

    if let error = errorOrNil {
        print(error.localizedDescription)
        return
    }

    promptCheckupForNotification()

    completionHandler()
}

healthStore.execute(query)

关键点:

  • HKCategoryType.categoryType(forIdentifier:):取得 Walking Steadiness 通知事件类型。
  • HKObserverQuery:监听 HealthKit 中新写入的样本。
  • sampleType: notificationType:只监听行走稳定性通知事件。
  • predicate: nil:监听该类型的所有新事件。
  • errorOrNil:处理监听回调中的错误。
  • promptCheckupForNotification():在应用中提示用户安排线上复诊。
  • completionHandler():通知 HealthKit 事件处理完成。
  • healthStore.execute(query):开始执行观察查询。

6. 查询六周数据,自己判断下降趋势

15:12)系统通知只覆盖 Low 和 Very Low。物理治疗诊所还可能关心另一类患者:分类还没变差,但分数已经连续下降。演讲中的做法是查询过去六周的 Walking Steadiness 样本,计算 best-fit slope(最佳拟合斜率),如果每周平均下降超过 5 点,就提示复查。

import HealthKit

let healthStore = HKHealthStore()
let end = Date()
let start = Calendar.current.date(byAdding: .weekOfYear, value: -6, to: end)
let datePredicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])

let steadinessType = HKObjectType.quantityType(forIdentifier: .walkingSteadiness)!
let sortByEndDate = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)

let query = HKSampleQuery(sampleType: steadinessType,
                          predicate: datePredicate,
                          limit: HKObjectQueryNoLimit,
                          sortDescriptors: [sortByEndDate]) { query, samples, error in

    if let error = error {
        print(error.localizedDescription)
        return
    }

    detectTrends(samples)
}

healthStore.execute(query)

关键点:

  • end = Date():查询窗口结束于当前时间。
  • Calendar.current.date(byAdding:):把开始时间设为六周前。
  • HKQuery.predicateForSamples(withStart:end:options:):限制 HealthKit 查询时间范围。
  • steadinessType:目标样本仍然是 Walking Steadiness 分数。
  • limit: HKObjectQueryNoLimit:取回窗口内所有样本。
  • sortDescriptors:按结束时间倒序排列。
  • detectTrends(samples):把样本交给自定义趋势判断函数。

趋势函数由应用自己实现。下面的示例只展示演讲中的判断入口:计算斜率,小于 -5 时提示复查。

import HealthKit

func detectTrends(_ samples: [HKSample]?) {
    let quantitySamples = samples as? [HKQuantitySample] ?? []
    let slope = bestFitSlope(for: quantitySamples)

    if slope < -5 {
        promptCheckupForNotification()
    }
}

关键点:

  • samples as? [HKQuantitySample]:把 HealthKit 样本转换为数量样本数组。
  • bestFitSlope(for:):应用自定义函数,计算六周样本的最佳拟合斜率。
  • slope < -5:对应演讲中的阈值,表示平均每周下降超过 5 点。
  • promptCheckupForNotification():复用通知场景里的复诊提示。

7. 让用户持续产生 Walking Steadiness 样本

16:21)应用能不能监测稳定性,取决于 HealthKit 是否有样本。Apple 给了四条实践建议。第一,用户需要在健康 App 中设置身高;体重和年龄也推荐设置。第二,提示用户开启 Apple Walking Steadiness 通知。第三,提醒用户走路时随身带 iPhone,放在裤袋、夹克口袋或贴近身体的包里都可以。第四,用 Walking Speed 样本判断用户是否携带设备足够稳定;连续两周有 Walking Speed 样本,是一个良好信号。

import HealthKit

let healthStore = HKHealthStore()
let walkingSpeedType = HKObjectType.quantityType(forIdentifier: .walkingSpeed)!
let end = Date()
let start = Calendar.current.date(byAdding: .weekOfYear, value: -2, to: end)
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])

let query = HKSampleQuery(sampleType: walkingSpeedType,
                          predicate: predicate,
                          limit: HKObjectQueryNoLimit,
                          sortDescriptors: nil) { query, samples, error in

    if let error = error {
        print(error.localizedDescription)
        return
    }

    if samples?.isEmpty == true {
        promptUserToCarryPhoneWhileWalking()
    }
}

healthStore.execute(query)

关键点:

  • .walkingSpeed:演讲建议用 Walking Speed 样本判断用户是否满足携带 iPhone 的条件。
  • startend:查询最近两周。
  • predicateForSamples:限制查询窗口。
  • HKObjectQueryNoLimit:读取两周内全部 Walking Speed 样本。
  • samples?.isEmpty == true:没有样本时,说明用户可能没有按要求携带 iPhone。
  • promptUserToCarryPhoneWhileWalking():提示用户走路时随身携带手机。

核心启发

  1. 做什么:为术后患者做 Six-Minute Walk 恢复看板。 为什么值得做:重新校准 API 能让术后估算更快反映急性变化,避免术前数据稀释术后下降。 怎么开始:请求 sixMinuteWalkTestDistance 的读写权限,拿到手术日期后调用 HKHealthStore.recalibrateEstimates(sampleType:date:),再读取后续样本画恢复曲线。

  2. 做什么:为物理治疗诊所做跌倒风险提醒。 为什么值得做:Walking Steadiness 已经给出 OK、Low、Very Low 分类,适合给护理团队做分层视图。 怎么开始:读取 .walkingSteadiness 最新样本,用 HKAppleWalkingSteadinessClassification(for:) 转换分类,把 Low 和 Very Low 患者放入复诊队列。

  3. 做什么:为远程康复 App 增加趋势下降预警。 为什么值得做:系统事件覆盖高风险分类,但分数下滑可以更早暴露训练不适合、用户未坚持或伤情变化。 怎么开始:每周查询过去六周 .walkingSteadiness 样本,计算趋势斜率,低于阈值时提示线上复查。

  4. 做什么:为家人照护场景做长辈行走质量周报。 为什么值得做:Walking Steadiness 每周生成一次分数,适合用低频摘要展示变化,减少每天查看健康数据的负担。 怎么开始:在用户授权后读取最近数周分数,展示分类变化,并在 .appleWalkingSteadinessEvent 出现时提示用户主动联系家人。

  5. 做什么:为健身或徒步 App 做路线难度建议。 为什么值得做:演讲提到稳定性可用于推荐更贴合个人跌倒风险的 workouts 和 hikes。 怎么开始:读取 Walking Steadiness 分类,把 Low 或 Very Low 用户引导到平缓路线或低冲击训练,把高风险说明放在开始训练前。

关联 Session

评论

GitHub Issues · utterances