Build a workout app for Apple Watch
Health & Fitness 进阶 32m

构建 Apple Watch 健身 app

Build a workout app for Apple Watch

2021年6月9日

在 Apple 官方观看视频

一句话判断

这场 Session 是 watchOS 健身 app 开发的完整蓝图——从 HKWorkoutSession 到新引入的 HKWorkoutBuilder,如果你在做任何运动追踪类产品,直接照着抄就行。

这场 Session 讲了什么

Apple Watch 是目前市面上最受欢迎的运动追踪设备之一,而构建一个优秀的健身 app 需要理解 HealthKit 的运动框架。这场 Session 从头搭建了一个健身 app,覆盖了运动生命周期管理、实时数据采集、暂停/恢复控制到运动结束后的数据保存。

Session 重点介绍了 HKWorkoutBuilder,这是 watchOS 8 中改进的运动数据构建器。与旧版直接往 HKWorkoutSession 塞数据不同,HKWorkoutBuilder 允许你在不启动运动会话的情况下预先收集数据,然后在合适的时机一次性提交。这个设计让数据采集和运动会话管理解耦了。

另外还讨论了运动类型的划分。HealthKit 将运动分为三类:有氧、力量和混合。不同类型影响系统如何计算卡路里消耗和心率区间。如果你把运动类型选错了,Fitness app 里的圆环数据会不准确。

值得深挖的点

HKWorkoutBuilder 与 HKWorkoutSession 的协作模式

旧版 API 中,HKWorkoutSession 同时负责会话管理和数据收集,这导致了生命周期管理的混乱。watchOS 8 把数据收集职责交给了 HKWorkoutBuilderHKWorkoutSession 只管会话状态(开始、暂停、恢复、结束)。你可以创建一个 HKWorkoutBuilder,在运动开始前就开始收集传感器数据(比如热身阶段),然后在正式开始运动时再启动 HKWorkoutSession

后台运动保活的正确姿势

Apple Watch 在运动模式下有特殊的后台权限。当 HKWorkoutSession 处于活跃状态时,你的 app 可以在后台持续运行、更新 UI、采集传感器数据。但这不是无限制的——如果你的 app 在后台超过一定时间没有调用 HKWorkoutSession 的相关 API,系统可能会终止你的进程。Session 建议在后台使用 WKExtendedRuntimeSession 来补充保活能力。

代码片段

// 创建并启动一个完整的运动会话
import HealthKit

class WorkoutManager: NSObject, HKWorkoutSessionDelegate {
    let healthStore = HKHealthStore()
    var session: HKWorkoutSession?
    var builder: HKWorkoutBuilder?
    
    func startWorkout(type: HKWorkoutActivityType) {
        let config = HKWorkoutConfiguration()
        config.activityType = type
        config.locationType = .outdoor  // 影响卡路里计算精度
        
        do {
            session = try HKWorkoutSession(
                healthStore: healthStore,
                configuration: config
            )
            builder = session?.associatedWorkoutBuilder()
            builder?.dataSource = HKLiveWorkoutDataSource(
                healthStore: healthStore,
                workoutConfiguration: config
            )
            
            session?.delegate = self
            builder?.beginCollection(withStart: Date()) { success, error in
                if success {
                    self.session?.startActivity(with: Date())
                }
            }
        } catch {
            print("创建运动会话失败: \(error)")
        }
    }
}
// 实时采集心率并更新 UI
func workoutBuilderDidCollectEvent(
    _ workoutBuilder: HKWorkoutBuilder
) {
    // 从 builder 中获取最新的心率样本
    guard let heartRateType = HKObjectType.quantityType(
        forIdentifier: .heartRate
    ) else { return }
    
    let predicate = HKQuery.predicateForObjects(
        from: workoutBuilder.workout
    )
    
    let query = HKStatisticsQuery(
        quantityType: heartRateType,
        quantitySamplePredicate: predicate,
        options: .discreteAverage
    ) { _, statistics, _ in
        let bpm = statistics?.averageQuantity()?
            .doubleValue(for: HKUnit.count().unitDivided(by: .minute()))
        DispatchQueue.main.async {
            // 更新表盘上的心率显示
            print("实时心率: \(bpm ?? 0)")
        }
    }
    healthStore.execute(query)
}
// 结束运动并保存数据
func endWorkout() {
    session?.end()
    builder?.endCollection(withEnd: Date()) { success, error in
        guard success else { return }
        self.builder?.finishWorkout { workout, error in
            if let workout = workout {
                // 运动记录已保存到 HealthKit
                print("运动已保存,时长: \(workout.duration / 60) 分钟")
            }
        }
    }
}

最佳实践

运动类型的 locationType 设置直接影响卡路里计算。室内跑步和室外跑步使用不同的算法——室外模式会结合 GPS 和海拔数据,精度更高但耗电更多。如果你的 app 同时支持室内和室外模式,让用户选择而不要自动判断。

保存运动数据前,用 HKWorkoutBuilder.seriesBuilder(for:) 检查各个数据序列是否完整。如果有传感器数据缺失(比如用户在运动中暂时遮挡了心率传感器),在 UI 上用虚线或灰色标记缺失区间,而不是假装数据不存在。

还有什么值得关注

  • watchOS 8 新增了 HKWorkoutRoute 的批量写入能力,可以一次写入整条路线的 GPS 坐标。
  • 搭配 HKLiveWorkoutBuilder 可以在 iPhone 端实时展示 Watch 采集的运动数据。
  • Swift Concurrency 在 watchOS 8 上可用,但 HealthKit 的回调仍然是 completion-based 的,需要自己封装 async wrapper。
WWDC 2021