构建 Apple Watch 健身 app
Build a workout app for Apple Watch
2021年6月9日
一句话判断
这场 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 把数据收集职责交给了 HKWorkoutBuilder,HKWorkoutSession 只管会话状态(开始、暂停、恢复、结束)。你可以创建一个 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。