【Swift/watchOS/HealthKit】Workout(ワークアウト)の実装方法!HKWorkoutSession

この記事からわかること
- Swift/watchOS/iOSでHealthKitフレームワークの使い方
- Workout(ワークアウト)とは?
- HKWorkoutSessionの実装方法
- 開始や中断、保存するには?
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- watchOS:11.2
- macOS:Sonoma 14.6.1
Workout(ワークアウト)とは?
「Workout(ワークアウト)」とはウォーキングやランニング、ヨガ、水泳などの運動のアクティビティ情報を計測することができるAppleデバイスの機能です。主にApple Watchで活用することができ、専用の「ワークアウト」アプリを使用することでApple Watchを装着して運動するだけで、特定の運動の記録データ保存し、計測することが可能になります。
iPhoneからでも「ヘルスケア」アプリ>「ブラウザ」タブ>「アクティビティ」>「ワークアウト」>「データを追加する」から特定のワークアウトデータを追加することが可能ですが、これは完全に手動でデータを追加する仕組みになっているのでwatchのように自動計測してくれるわけではないようです。

アプリ開発とWorkout
アプリ開発におけるWorkoutはHealthKit
フレームワーク内のHKWorkoutSession
という単位で管理されています。HealthKit
自体の使い方に関してはデータアクセスへの許可申請やデータの取得方法には癖があるので前知識として以下の記事を参考にしてください。
Apple Watch(watchOS)アプリ自体の開発方法などは以下の記事を参考にしてください。
またAppleがデモアプリを公開してくれているのでこちらも参考にしてください。
WorkoutをwatchOSアプリから操作する
公式リファレンス:Running workout sessions
watchOSアプリからWorkoutを開始・終了・保存など操作する方法をまとめていきます。ただそのためにまず前準備として「info.plist」への値の追加や各項目へのアクセス許可申請などやっておくべきことがあるので先に準備しておきます。
前環境準備はこちらの記事を参考にしてください。ここではwatchOSターゲットの「info.plist」に追加するキーだけ紹介しておきます。
- NSHealthUpdateUsageDescription:データの保存
- NSHealthShareUsageDescription:データの読み取り
- NSHealthClinicalHealthRecordsShareUsageDescription:臨床データの読み取り(※)
- NSHealthRequiredReadAuthorizationTypeIdentifiers:読み取る臨床データタイプ(※)
ワークアウトデータへのアクセス許可申請
許可申請するべき項目は以下の通りになります。許可申請の実装については「HealthKit#データへの許可要求」を参考にしてください。ここでは項目のみ記述しておきます。
/// 書き込み許可申請項目
public let writeAllTypes: Set = [
// ワークアウト
HKQuantityType.workoutType()
]
/// 読み取り許可申請項目
public let readAllTypes: Set = [
// 消費エネルギー
HKQuantityType(.activeEnergyBurned),
// サイクリングの移動距離
HKQuantityType(.distanceCycling),
// ウォーキング・ランニングの移動距離
HKQuantityType(.distanceWalkingRunning),
// 車椅子ユーザーの移動距離
HKQuantityType(.distanceWheelchair),
// 心拍数
HKQuantityType(.heartRate),
// ワークアウト
HKQuantityType.workoutType()
]
Background Modeの有効
ワークアウトはApple Watchがバックグラウンド状態でも動作する必要があるため「Background Mode」を有効にする必要があります。「Signing & Capabilities」から「Background Modes」を追加し「Workout processing」にチェックを入れておきます。

Workoutを管理する主要なクラス
HKWorkoutSession
・・・Workoutの開始・終了・一時停止などを全体の操作を管理
HKLiveWorkoutBuilder
・・・Workout中に収集されるデータを管理
Workoutを開始する
Watch開発アプリからWorkoutを実行するためには以下のステップで実装します。
- HKWorkoutConfiguration設定
- HKWorkoutSessionとHKLiveWorkoutBuilderの取得
- デリゲートの設定
- データソースの設定
- Workoutを起動
実装サンプルコードはGitHubで公開しているので参考にしてください。
1.HKWorkoutConfiguration設定
Workoutを開始するためにはHKWorkoutConfiguration
で設定を行います。activityType
でアクティビティの種類(ウォーキングやランニングなど)をlocationType
で屋外/屋内を指定します。
let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor
2.HKWorkoutSessionとHKLiveWorkoutBuilderの取得
Workoutを実際に開始するにはまずHKWorkoutSession
をインスタンス化します。引数にHKHealthStore
とHKWorkoutConfiguration
を渡します。次にassociatedWorkoutBuilder
メソッドを呼び出しHKLiveWorkoutBuilder
を取得します。
do {
session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
builder = session.associatedWorkoutBuilder()
} catch {
// Handle failure here.
return
}
3.デリゲートの設定
HKWorkoutSessionDelegate
とHKLiveWorkoutBuilderDelegate
からそれぞれイベントを検知することができます。必要であればdelegate
の設定をしておきます。
session.delegate = self
builder.delegate = self
4.データソースの設定
リアルタイムのWorkoutデータを取得できるようにHKLiveWorkoutDataSource
を設定します。
builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
5.Workoutを起動
準備が整ったのでセッションとビルダーの両方を起動させます。HKWorkoutSession
はstartActivity
、HKLiveWorkoutBuilder
はbeginCollection
を使用します。Workoutが実行中の際はUIに実行中であることが識別できるような実装をする必要があるようです。
let startDate = Date()
session?.startActivity(with: startDate)
builder?.beginCollection(withStart: startDate) { [weak self] (success, error) in
guard let self else { return }
guard success else {
print("ワークアウト開始失敗:", error)
return
}
print("ワークアウト開始")
}
Workoutを操作する
起動しているWorkoutは中断・再開・終了など操作することが可能です。
中断
session?.pause()
再開
session?.resume()
終了
Workoutを終了する際はセッションとビルダーの両方を終了させる必要があります。HKWorkoutSession
はend
、HKLiveWorkoutBuilder
はendCollection
を実行し、その中でさらにfinishWorkout
を実行します。
session?.end()
builder?.endCollection(withEnd: Date()) { [weak self] (success, error) in
guard let self else { return }
guard success else {
print("ワークアウト終了失敗:", error)
return
}
self.builder?.finishWorkout { [weak self] (workout, error) in
guard let self else { return }
guard workout != nil else {
print("ワークアウト終了失敗:", error)
return
}
print("ワークアウト終了")
}
}
HKWorkoutSessionDelegate
公式リファレンス:HKWorkoutSessionDelegate
HKWorkoutSessionDelegate
はセッションに関する変化やエラーを検知するデリゲートです。workoutSession(_:didChangeTo:from:date:)
とworkoutSession(_:didFailWithError:)
の実装が必須になっています。
extension WorkoutManager: HKWorkoutSessionDelegate {
/// セッションの状態が変化した際に呼ばれる
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
self.log.append("セッション状態変化:\(toState)\n")
self.log.append("セッション状態変化:\(fromState)\n")
}
/// セッションがエラーで失敗した際に呼ばれる
func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: any Error) {
self.log.append("セッションエラー:\(error)\n")
}
}
HKWorkoutSessionState
状態の変化はHKWorkoutSessionState
型で取得できます。
public enum HKWorkoutSessionState : Int, @unchecked Sendable {
// 開始されていない
case notStarted = 1
// セッション実行中
case running = 2
// 終了
case ended = 3
// 一時停止
case paused = 4
// 準備中
case prepared = 5
// 停止
case stopped = 6
}
HKLiveWorkoutBuilderDelegate
公式リファレンス:HKLiveWorkoutBuilderDelegate
HKLiveWorkoutBuilderDelegate
はヘルスケアデータやイベントが追加されたことを検知するデリゲートです。workoutBuilder(_:didCollectDataOf:)
とworkoutBuilderDidCollectEvent(_:)
の実装が必須になっています。
extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
/// ヘルスケアデータが追加された際に呼ばれる
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
}
/// イベントが追加された際に呼ばれる
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。