WWDC Quick Look 💓 By SwiftGGTeam
Build a multi-device workout app

Build a multi-device workout app

观看原视频

Highlight

Apple 在 iOS 17、watchOS 10 和 iPadOS 17 中为 HealthKit 加入了三项核心能力:Apple Watch 与 iPhone 之间的实时运动镜像会话、骑行专用数据类型(踏频、功率、速度、FTP)、以及 iPad 端 HealthKit 支持,开发者可以用一套 API 构建覆盖三端设备的运动 App。


核心内容

问题:运动 App 只能活在手表上

如果你开发过 Apple Watch 运动 App,你一定经历过这个场景:用户在户外骑车,手表绑在手腕上,想看骑行数据得低头抬腕——骑行时这个动作既不安全也不方便。更大的屏幕(比如固定在车把上的 iPhone 或室内骑行架前的 iPad)放着不用,因为 HealthKit 的实时数据只能在手表上获取。

更麻烦的是,骑行产生的数据类型远不止心率和卡路里。踏频(每分钟踏板转数)、功率(瓦特输出)、速度——这些数据在 watchOS 9 之前,HealthKit 没有直接支持。开发者要么放弃这些指标,要么自己对接蓝牙传感器、手动解析数据、再自己想办法存回 HealthKit。

蓝牙传感器的兼容性是另一个坑。每个品牌的数据格式和配对流程都不同,用户需要单独购买、配对、校准。对开发者来说,这意味着维护一套与健身无关的蓝牙通信代码。

Apple 的方案:三端联动 + 骑行原生支持

在 WWDC 2023,Apple 一次性解决了这些问题,给出了三个关键能力:

  1. 镜像运动会话(Mirrored Workout Session):手表上的实时运动数据可以镜像到 iPhone,状态自动同步,iPhone 端可以展示更丰富的数据面板。
  2. 骑行专用数据类型:HealthKit 直接支持踏频、功率、速度、FTP(功能性阈值功率),手表可以自动从蓝牙传感器采集这些数据并存入 HealthKit。
  3. iPad 端 HealthKit:HealthKit 和 Health App 登陆 iPad,通过 iCloud 同步健康数据,iPad 可以作为大屏运动数据面板。

这三个能力互相配合:手表负责采集数据,iPhone 做实时镜像显示,iPad 做大屏回顾和分析。一次骑行产生的数据,在所有设备上都可用。


详细内容

镜像运动会话:手表采集,手机显示

这是本次更新中最重要的新 API。它的工作方式是这样的:手表上运行一个 HKWorkoutSession 作为主会话(Primary Session),调用 startMirroringToCompanionDevice() 后,配对的 iPhone 会自动启动配套 App,接收一个镜像会话(Mirrored Session)(04:15)。

镜像会话会同步主会话的所有状态变更——开始、暂停、恢复、结束——开发者不需要手动管理两边的状态。

在手表端发起镜像:

// Apple Watch — 启动运动并开启镜像
let configuration = HKWorkoutConfiguration()
configuration.activityType = .cycling
configuration.locationType = .outdoor

let session = HKWorkoutSession(
    healthStore: healthStore,
    configuration: configuration
)

let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(
    healthStore: healthStore,
    workoutConfiguration: configuration
)

// 开始运动
session.startActivity(with: Date())
builder.beginCollection(withStart: Date()) { success, error in
    // 数据采集已开始
}

// 开启向 iPhone 的镜像
try await healthStore.startMirroringToCompanionDevice(session)

关键点:

  • HKWorkoutConfiguration 定义运动类型和场地(户外/室内),影响系统如何采集 GPS 和高度数据。
  • HKLiveWorkoutDataSource 是手表端实时数据的入口,它自动从手表传感器和已配对的蓝牙设备收集数据。
  • startMirroringToCompanionDevice() 只能在手表端调用,要求配套 iPhone App 已安装且同一 iCloud 账号。
  • 手表 App 必须保持在前台,镜像才会持续活跃。

在 iPhone 端,通过锚点查询接收实时数据:

// iPhone — 接收镜像数据
class MirroringManager: ObservableObject {
    let healthStore = HKHealthStore()
    @Published var heartRate: Double = 0

    func observeMirroredData() {
        let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!

        let query = HKAnchoredObjectQuery(
            type: heartRateType,
            predicate: nil,
            anchor: nil,
            limit: HKObjectQueryNoLimit
        ) { _, samples, _, _, _ in
            guard let quantitySamples = samples as? [HKQuantitySample] else { return }
            for sample in quantitySamples {
                let bpm = sample.quantity.doubleValue(for: HKUnit(from: "count/min"))
                DispatchQueue.main.async {
                    self.heartRate = bpm
                }
            }
        }

        healthStore.execute(query)
    }
}

关键点:

  • HKAnchoredObjectQuery 会在新数据到达时自动回调,不需要轮询。
  • 镜像会话启动后,iPhone App 会在后台被拉起,有约 10 秒的时间来处理会话设置。
  • 所有 HealthKit 查询结果在后台线程返回,更新 UI 前必须切换到主线程。

手表和 iPhone 之间还可以通过 sendData 互相发送自定义数据(06:30):

// 手表向 iPhone 发送自定义事件
let lapData = "lapCompleted".data(using: .utf8)!
try await healthStore.sendData(lapData, toRemoteWorkoutSession: session)

// iPhone 端接收 — 通过 HKWorkoutSessionDelegate
func workoutSession(_ workoutSession: HKWorkoutSession,
                    didReceiveDataFromRemoteWorkoutSession data: [Data]) {
    for item in data {
        if let message = String(data: item, encoding: .utf8) {
            print("收到手表消息: \(message)")
        }
    }
}

关键点:

  • sendData 支持在两端之间传递任意二进制数据,适合发送自定义事件(圈数标记、区间切换、用户提示)。
  • 接收端通过 HKWorkoutSessionDelegatedidReceiveDataFromRemoteWorkoutSession 回调获取数据。
  • 这不是高频数据传输通道,适合少量事件级数据,不适合传输连续的传感器数据流。

骑行数据类型:不再需要自己对接传感器

watchOS 10 中,HealthKit 新增了四个骑行专用数据类型(08:20):

数据类型HealthKit Identifier单位
踏频cyclingCadence转/分钟 (count/min)
功率cyclingPower瓦特 (W)
速度cyclingSpeed米/秒 (m/s)
FTPcyclingFunctionalThresholdPower瓦特 (W)

Apple Watch 可以直接连接蓝牙骑行传感器(功率计、踏频传感器),自动采集数据并写入 HealthKit。开发者不需要写一行蓝牙代码。

配置数据源时,启用对应的数据类型即可:

let dataSource = HKLiveWorkoutDataSource(
    healthStore: healthStore,
    workoutConfiguration: configuration
)

// 启用踏频采集
dataSource.enableCollection(
    for: HKQuantityType(.cyclingCadence),
    predicate: nil
)

// 启用功率采集
dataSource.enableCollection(
    for: HKQuantityType(.cyclingPower),
    predicate: nil
)

// 启用速度采集
dataSource.enableCollection(
    for: HKQuantityType(.cyclingSpeed),
    predicate: nil
)

关键点:

  • enableCollection 告诉 HKLiveWorkoutDataSource 开始采集指定类型的数据。如果不调用,即使传感器已连接,数据也不会被记录。
  • 手表会自动处理蓝牙传感器的配对和数据读取,配对流程在系统设置中完成,不需要 App 介入。
  • 这些数据类型在请求授权时需要加入 typesToRead 集合。

实时处理骑行数据——通过 HKWorkoutBuilderaddSamplesHandler10:45):

builder.addSamplesHandler = { [weak self] samples in
    for sample in samples {
        switch sample.sampleType {
        case HKQuantityType(.cyclingCadence):
            if let quantity = (sample as? HKQuantitySample)?.quantity {
                let rpm = quantity.doubleValue(for: HKUnit(from: "count/min"))
                self?.cadence = Int(rpm)
            }
        case HKQuantityType(.cyclingPower):
            if let quantity = (sample as? HKQuantitySample)?.quantity {
                let watts = quantity.doubleValue(for: .watt())
                self?.power = Int(watts)
            }
        case HKQuantityType(.cyclingSpeed):
            if let quantity = (sample as? HKQuantitySample)?.quantity {
                let mps = quantity.doubleValue(for: HKUnit.meter().unitDivided(by: .second()))
                self?.speed = mps
            }
        default:
            break
        }
    }
}

关键点:

  • addSamplesHandler 在每次有新样本时回调,适合实时 UI 更新。
  • 每个 HKQuantitySample 包含数值和时间戳,可以用 doubleValue(for:) 转换为需要的单位。
  • FTP 由 HealthKit 根据多次骑行数据自动估算,不需要用户手动做 20 分钟全力骑测试。
  • [weak self] 捕获列表避免循环引用——builder 持有 handler,handler 不应强引用持有 builder 的对象。

HealthKit on iPad:大屏健康数据面板

iPadOS 17 首次引入了 HealthKit 支持和 Health App。虽然 iPad 没有心率传感器和 GPS,但它可以通过 iCloud 同步获取其他设备上的健康数据(14:10)。

iPad 上请求 HealthKit 授权的方式与 iPhone 不同,需要使用 HealthKitUI 框架:

import HealthKitUI
import SwiftUI

struct WorkoutView: View {
    let healthStore = HKHealthStore()
    let typesToShare: Set<HKSampleType> = [.workoutType()]
    let typesToRead: Set<HKObjectType> = [
        HKObjectType.quantityType(forIdentifier: .cyclingCadence)!,
        HKObjectType.quantityType(forIdentifier: .cyclingPower)!,
        HKObjectType.quantityType(forIdentifier: .cyclingSpeed)!,
    ]

    var body: some View {
        VStack {
            // 运动数据展示 UI
        }
        .healthDataAccessRequest(
            store: healthStore,
            shareTypes: typesToShare,
            readTypes: typesToRead,
            trigger: true,
            completion: { result in
                switch result {
                case .success:
                    print("授权成功")
                case .failure(let error):
                    print("授权失败: \(error)")
                }
            }
        )
    }
}

关键点:

  • iPad 上 HealthKit 数据来自 iCloud 同步,不是实时采集,适合做运动后的回顾和分析。
  • 授权 UI 必须使用 HealthKitUI 框架的 healthDataAccessRequest(SwiftUI)或 HKHealthStoreauthorizationViewControllerPresenter(UIKit)。
  • 在 iPad 上需要处理多窗口场景——用户可能从不同窗口触发授权请求。
  • HKHealthStore.isHealthDataAvailable() 在 iPadOS 17 之前的 iPad 上返回 false,需要做版本检查。

在 iPad 上查询历史骑行数据:

let predicate = HKQuery.predicateForWorkouts(with: .cycling)
let sortDescriptor = NSSortDescriptor(
    key: HKSampleSortIdentifierStartDate,
    ascending: false
)

let query = HKSampleQuery(
    sampleType: .workoutType(),
    predicate: predicate,
    limit: 1,
    sortDescriptors: [sortDescriptor]
) { _, samples, error in
    guard let workout = samples?.first as? HKWorkout else { return }

    let distance = workout.totalDistance?.doubleValue(
        for: HKUnit.meterUnit(with: .kilo)
    )
    let energy = workout.totalEnergyBurned?.doubleValue(
        for: .kilocalorie()
    )

    DispatchQueue.main.async {
        self.distance = distance
        self.calories = energy
    }
}

healthStore.execute(query)

关键点:

  • predicateForWorkouts(with:) 按运动类型过滤,返回所有该类型的 workout。
  • HKWorkout 自带 totalDistancetotalEnergyBurned 属性,不需要额外查询。
  • 要获取踏频、功率等详细指标,需要用 HKStatisticsQuery 按 workout 的 UUID 过滤查询。

核心启发

1. 做一个车把手机仪表盘

做什么:iPhone App 作为骑行时的实时仪表盘,通过镜像会话从手表获取踏频、功率、速度、心率,以大字体和全屏模式显示在固定在车把上的 iPhone 屏幕上。

为什么值得做startMirroringToCompanionDevice() 让 iPhone 可以零延迟获取手表传感器数据。配合 HKLiveWorkoutDataSourceenableCollection,所有骑行指标自动到位,不需要自己写任何传感器对接代码。

怎么开始:手表端创建 .cycling 类型的 HKWorkoutSession,调用 startMirroringToCompanionDevice()。iPhone 端用 HKAnchoredObjectQuery 监听实时数据,SwiftUI 用大号 TextGauge 组件展示各项指标。

2. 做一个室内骑行 iPad 训练面板

做什么:iPad App 展示用户最近的骑行数据趋势,包括 FTP 变化曲线、功率区间分布、踏频稳定性分析,配合 Apple Watch 实时采集的数据做训练指导。

为什么值得做:iPad 大屏天然适合展示复杂数据。HealthKit 在 iPadOS 17 上可用后,所有手表采集的骑行数据通过 iCloud 自动同步到 iPad。FTP 由系统自动估算,用户不需要手动输入。

怎么开始:在 iPad 上用 HKSampleQuery 查询历史 .cycling 类型的 workout,提取 cyclingPowercyclingCadencecyclingFunctionalThresholdPower 等指标。用 Swift Charts 绘制功率曲线和 FTP 变化趋势。

3. 做一个骑行社交 App 的数据采集层

做什么:利用 HealthKit 骑行数据作为社交 App 的数据来源,自动记录每次骑行的路线、功率、速度,不需要用户手动输入或依赖第三方传感器 App。

为什么值得做:之前骑行 App 要么自己做传感器对接,要么依赖 Strava 等第三方数据。现在 HealthKit 直接提供踏频、功率、速度,开发者可以聚焦在社交功能上,数据采集层完全交给系统。

怎么开始:请求 .cyclingCadence.cyclingPower.cyclingSpeed 的读权限,用 HKStatisticsCollectionQuery 按天/周聚合数据。骑行路线通过 HKWorkoutRouteBuilder 关联 GPS 数据。注意用户隐私:只在用户明确授权后才读取数据。

4. 做一个 FTP 自动追踪工具

做什么:利用 HealthKit 自动估算的 FTP 值,追踪用户骑行能力的长期变化,在 FTP 提升时给出提醒和激励。

为什么值得做:FTP 是衡量骑行能力最核心的指标,但传统方式需要用户定期做 20 分钟全力骑测试——痛苦且不准确。HealthKit 根据日常骑行数据自动估算 FTP,数据更真实、更持续。

怎么开始:用 HKAnchoredObjectQuery 监听 cyclingFunctionalThresholdPower 的变化,记录每次 FTP 更新的时间点和数值。用 Swift Charts 绘制长期趋势线。在 FTP 突破新值时发送本地通知。


关联 Session

  • Meet watchOS 10 — watchOS 10 的整体更新,包括新的设计语言和 Smart Stack,是理解本 session 中手表端 API 的前提。
  • Update your app for watchOS 10 — 将现有 watchOS App 迁移到 watchOS 10 的实践指南,涵盖 SwiftUI 新组件和布局模式。
  • Platforms State of the Union — WWDC 2023 平台技术总览,了解 HealthKit 更新在各平台生态中的位置。

评论

GitHub Issues · utterances