
-
iOSとiPadOSでのHealthKitによるワークアウトのトラッキング
iOS上で最適なワークアウト体験を構築するためのベストプラクティスを紹介します。ワークアウトセッションのライフサイクル、Apple WatchとiPhoneの間のワークアウトの差異、アプリのロック画面の体験を向上させるためのライブアクティビティとSiriの利用方法について学ぶことができます。
関連する章
- 0:00 - イントロダクション
- 0:56 - ワークアウトセッションの実行
- 2:50 - セッションメトリックスの取得
- 8:35 - クラッシュからのリカバリ
- 9:34 - ベストプラクティス
リソース
- Building a multidevice workout app
- Building a workout app for iPhone and iPad
- Handling Workout Requests with SiriKit
- HKWorkoutSession
- Running workout sessions
関連ビデオ
WWDC25
WWDC24
WWDC23
-
このビデオを検索
こんにちは HealthKitチームのエンジニア Brianです 世の中には 健康増進や 健康維持を目的とした ヘルスアプリやフィットネスアプリは 沢山あります ユーザーの許可を得て これらアプリは HealthKitの 一元化された暗号化データベースや パワフルなAPIにアクセスして 総合的な健康状態を提供します ワークアウトAPIは HealthKitが提供する 最も強力な機能の一つに成長しました 本日は iPhoneとiPadで APIを使用する方法をご紹介します まず ワークアウトセッション実行に関する 基本事項を説明します 次に ワークアウト中の測定値への アクセス方法を 詳しく説明します Apple Watchのプロセスとの違いにも 注目してみます 次に クラッシュが発生した場合の 回復方法を紹介します 最後に ワークアウトのヒントや ベストプラクティスを紹介します ワークアウトセッションを始めましょう すでにApple Watchのアプリで ワークアウトを実行している場合は iPhoneとiPadでも最小限の変更だけで 同じコードを使用できます Apple Watchと同様 ワークアウトセッションで あらゆるアクティビティをトラッキングし 関連付けしたWorkout Builderによる HealthKitへの保存が可能になりました Apple Watchアプリを これから開発される皆さんのために ワークアウトセッション実行方法の例を 簡単に説明します ワークアウトセッションは セットアップから開始、測定値の収集まで 複数のステップに分かれ 最後にセッション終了です まず ワークアウト構成を作成して ユーザーが実行したいと思っている アクティビティを反映させて タイプを設定します 「running」を使用して 場所を「outdoor」に設定します HKWorkoutSessionを作成して 設定を渡します
ワークアウトセッションから 関連付けられているビルダーを取得して データソースをアタッチします
セッションで「prepare」を呼び出し 3秒のカウントダウンを表示して デバイス上のセンサーがオンになるまで または外部心拍計を接続するまでの 時間を確保します カウントダウンがあることで ワークアウトアクティビティの開始後すぐに 測定値が利用可能になります カウントダウンが完了したら セッションでstartActivityを 関連するWorkout Builderで beginCollectionを呼び出すだけです アンカーオブジェクトクエリを使用して UIを更新する必要はありません 新しいデータを収集すると Workout Builderデリゲートは アップデートをアプリに提供し ワークアウトの保存時には 測定値を同期処理します
アプリを使用しているユーザーが ワークアウトを終了すると判断した場合は ライブビルダーで 最終的な測定値を収集できるよう セッションでstopActivityを 呼び出す必要があります
セッションが停止状態に遷移したら ビルダーで「endCollection」を呼び出して ワークアウトを終了できます ビルダーが完成したら セッションで「end」を呼び出し ワークアウトの概要を表示します
ワークアウトの実行方法を 一通りご紹介したところで Apple Watchで実行する場合と iPhoneやiPadで実行する場合の 違いについて説明します 第一の 主な違いは 利用可能なセンサーです
iPhoneとiPadでは すべてのワークアウト アクティビティタイプを利用できますが これらデバイスには 心拍センサーが搭載されていません
ただし ウェアラブル心拍モニタや Powerbeats Pro 2など ワークアウト中に装着する GATプロファイル対応デバイスとの ペアリングは可能です
デバイスをペアリングすると HealthKitは心拍数データを デバイスから取得して サンプルとしてHealth Storeに保存し アプリで利用できる状態にします つまり ワークアウト中に 収集したいサンプルと システムが生成できるサンプルは 違っている可能性が あるということです その違いを見ていきましょう
先ほど 型を生成しました カロリーや距離など ワークアウト中に システムによって生成される データ型です 収集される型のほうは リアルタイムで監視して ワークアウトサンプルに追加する 測定値です たとえば ワークアウト中の 水分摂取量を収集する場合 アプリはHealthデータベースに サンプルを追加する必要があります
iPhoneやiPadの場合 初期化すると データソースで収集する型には 実行中のアクティビティで収集しそうな すべてのサンプルタイプが含まれます たとえば 外部心拍センサーがない場合 システムで生成しない場合でも 心拍数が含まれます
データソースは システムが生成したものであるか アプリが保存したものかを問わず 収集したサンプルとタイプを監視して ライブビルダーに渡します
システムが実際に生成している データを知りたい場合は UIの測定値更新に 使用しているものと同じ Workout Builderデリゲートを 使用できます
アプリで収集する デフォルトの型を変更する場合は データソースで型のメソッドに対して 「enable collection」や 「disable collection」を呼び出して 必要な型の追加や削除ができます
たとえば ワークアウト中の 水分摂取量を収集すると アプリが Enable Collectionを呼び出し ワークアウトの実行中に 測定値をサンプルとして Healthデータベースに追加し データソースはサンプルを自動的に ライブビルダーに渡します セッションの測定値を 取得する方法を紹介しました ここで ワークアウトの保存後に 測定値を読み取る方法を 簡単に説明します
まず ワークアウトオブジェクトの 統計値を使用して 概要を表示します
ワークアウト実行中の測定値を グラフにしたい場合は 任意の間隔を指定して 統計データ収集クエリを 使用します
詳細なデータが必要な場合は ワークアウトに関連する数量サンプルは 粒度が1以上になる場合もあることを 忘れないようにしましょう つまり サンプルが より細かい粒度のデータを持っていて HKQuantitySeriesSampleQueryで アクセスできる ということです
お気づきでしょうが これは 過去のApple Watch ワークアウトについての話でした ただしこれは iOSに保存されている ワークアウトにも当てはまります ここまで アプリの表示中に 測定値を収集して ワークアウト保存後に 読み取る方法を紹介しました もう1つの重要な相違点ですが Apple Watchとは異なり ワークアウト実行中 iPhoneは 高い確率でロックされます プライバシー上の理由から 通常 デバイスのロック中は 健康データを利用できませんが 心配は要りません 1回目のワークアウトセッション スタート時に システムは デバイスのロック中も アプリがワークアウトデータを 利用できることを示す プロンプトを提示します アプリでライブアクティビティを表示する 絶好の機会です iPhoneのロックを解除することなく 最も重要な測定値を ロック画面に表示して 最新情報をリアルタイムで 確認できるようになります 先ほど プライバシープロンプトについて お話ししました データにアクセスできない場合や デバイスがロックされているとき 心拍数データが利用できない場合は UIを更新して 測定値を非表示にし ワークアウトの時間だけを 表示させることができます
ロック画面からできることは 他にもあります ロック画面もSiri対応に なったことを嬉しく思います iPhoneのロックを解除しなくとも ワークアウトの開始、一時停止、再開と キャンセルが可能になりました ロックが解除されると HealthKitがワークアウトを保存して Health Store経由で アプリが利用できるようにします Siri Intentをアプリに追加して ロック画面からでも 機能させる方法を説明します
まず Intentハンドラの定義から スタートします ロック画面でも機能させるには アプリ内部からの処理が必要です 次に サポートするIntentを 定義します 読みやすくするために この例では分割しています 次に 引数として渡されるIntentです この例では StartWorkoutIntentです まず 実行中のワークアウトについて確認し ワークアウトがある場合は 「failure」を返します 次に アクティビティ型と場所を 取得する必要があります この例では 屋外でのランニングに設定します 成功レスポンスを返します
Intentを定義したら 応答できるように Appデリゲートを 作成する必要があります 次に アプリ内のデリゲートを 定義します
これで アプリがロック画面での Siri Workout Intentに対応します
Siri Intentとライブアクティビティを 追加しておくと デバイスの状態に 左右されることなく ユーザーがアプリを最大限に 活用できるようになります これら2つの技術の詳細については WWDC2024の「Bring your app's core features to users with App Intents」と WWDC23の「Meet ActivityKit」をご覧ください セッションの測定値を取得する方法と ライブアクティビティで Siri Intentの使用方法が 分かったところで Crash Recoveryに進みましょう Crash Recoveryは以前から Watchにあった機能ですが 重要なポイントを3つ確認しておきましょう クラッシュ発生時 システムは自動的に アプリを再起動させます Builderのワークアウトセッションは 再起動前の状態に戻ります ただし ライブデータソースの設定は やり直しが必要になります iPhoneとiPad用に 新しく セッション中のワークアウトの 回復に使用する シーンデリゲートを追加しました
Siriインテント用に作成した アプリデリゲートを使用して クラッシュからのリカバリ処理用の シーンデリゲートを追加できます その場合は Appデリゲートを定義して optionsパラメータが shouldHandleActiveWorkoutRecovery とあるか確認します 次に 復元された recoveredSessionを Health Storeから受け取って WorkoutManagerに 渡すよう指定します WorkoutManagerから アクティブなワークアウトを 継続するよう処理できます そう 後はdataSourceを 作成し直すだけです
最後になりますが ワークアウトの ベストプラクティスをご紹介します 測定値をすべて取得できるよう Watchアプリがあればそこで ワークアウトを開始するようにします Health StoreからStart Watch Appを呼び出すだけです その後は必ず ワークアウトを iPhoneにミラーリングします WWDC2023の「Build a multi-device workout app」で 詳しく紹介しています
承認リクエストは 必要なデータ型についてのみ 実行させるようにします アプリの本質とは無関係に見える データ型で承認を要求して ユーザーに疑念を持たせてしまうのは 得策ではありません 最後に ワークアウトの作成と 保存には Workout Builder APIを 使用するようにします これで アクティビティリングが 適切に更新されるようになります
以上が iPhoneやiPadでの HealthKitによる ワークアウトのトラッキング方法です アップデートを機に これらデバイス用の 堅牢なAPIが搭載され クラッシュからのリカバリ機能と ワークアウト表示の管理機能が加わり デバイスのロック中も操作できます ここで お願いがあります 本セッション用のデモアプリは 是非ともダウンロードをお願いします 本日紹介したすべてのコードの機能を すぐに試していただけるよう ラップされています すでにiPhoneまたはiPad用の アプリを構築されている場合は 本日紹介した Workout Builder APIへの アップグレードを強くお勧めします 改善ポイントを 気に入っていただけると思います Apple Watch用のアプリを 構築されている場合は 同じAPIでマルチプラットフォームに 対応することで 非Apple Watchユーザー向けの まったく新しい市場が開けてきます 最後に 実装した機能は フィードバックから生まれました 皆さんからの引き続きのご提供を 心よりお待ちしています 世界を健康的にするための アプリ構築に必要な機能を提供して これからも皆さんをサポートします ご視聴ありがとうございました
-
-
1:30 - Set up workout session
// Set up workout session // Create workout configuration let configuration = HKWorkoutConfiguration() configuration.activityType = .running configuration.locationType = .outdoor // Create workout session let session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration) session.delegate = self // Get associated workout builder and add data source let builder = session.associatedWorkoutBuilder() builder.delegate = self builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
-
1:54 - Starting the session
// Prepare and start session session.prepare() // Start and display count down // Start session and builder collection once count down finishes session.startActivity(with: startDate) try await builder.beginCollection(at: startDate)
-
2:14 - Handling Metrics
// Handling collected metrics func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) { for type in collectedTypes { guard let quantityType = type as? HKQuantityType else { return } let statistics = workoutBuilder.statistics(for: quantityType) // Update the published values updateForStatistics(statistics) } }
-
2:28 - Ending workout
// Stopping the workout session session.stopActivity(with: .now) // Session transitions to stopped then call end func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { guard change.newState == .stopped, let builder else { return } try await builder.endCollection(at: change.date) let finishedWorkout = try await builder.finishWorkout() session.end() }
-
7:17 - Set up Siri Intent
// Create an INExtension within your main app // Define an intent handler public class IntentHandler: INExtension { } // Define the intents to support extension IntentHandler: INStartWorkoutIntentHandling extension IntentHandler: INPauseWorkoutIntentHandling extension IntentHandler: INResumeWorkoutIntentHandling extension IntentHandler: INEndWorkoutIntentHandling
-
7:32 - Handle the Siri intent
// Handle the intent public func handle(intent: INStartWorkoutIntent) async -> INStartWorkoutIntentResponse { let state = await WorkoutManager.shared.state switch state { case .running, .paused, .prepared, .stopped: return INStartWorkoutIntentResponse(code: .failureOngoingWorkout, userActivity: nil) default: break; } Task { await MainActor.run { // Handle the intents activity type and location WorkoutManager.shared.setWorkoutConfiguration(activityType: .running, location: .outdoor) } } return INStartWorkoutIntentResponse(code: .success, userActivity: nil) }
-
7:52 - App Delegate
// Implement an app delegate // Create app delegate class WorkoutsOniOSSampleAppDelegate: NSObject, UIApplicationDelegate { let handler = IntentHandler() func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? { return handler } } // Add app delegate to app struct WorkoutsOniOSSampleApp: App { @UIApplicationDelegateAdaptor(WorkoutsOniOSSampleAppDelegate.self) var appDelegate }
-
9:09 - Set up crash recovery
// App Delegate func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { if options.shouldHandleActiveWorkoutRecovery { let store = HKHealthStore() store.recoverActiveWorkoutSession(completion: { (workoutSession, error) in // Handle error Task { await WorkoutManager.shared.recoverWorkout(recoveredSession: workoutSession) } }) } let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) configuration.delegateClass = WorkoutsOniOSSampleAppSceneDelegate.self return configuration }
-
9:25 - Recover the workout session
// Recover the workout for the session func recoverWorkout(recoveredSession: HKWorkoutSession) { session = recoveredSession builder = recoveredSession.associatedWorkoutBuilder() session?.delegate = self builder?.delegate = self workoutConfiguration = recoveredSession.workoutConfiguration let dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: workoutConfiguration) builder?.dataSource = dataSource }
-