🩺

【iOS】Workout Session入門

2024/06/10に公開

Workout・WorkoutSessionとは?

AppleWatchの1つの機能である「Workout」です。アクティビティが日常生活の活動量を常時計測するのに対して、Workoutはランニングやウォーキング、ジムでのトレーニングなど、運動を計測します。AppleWatchでWorkoutを追跡するには、WorkoutSessionを設定、開始、保存する必要があります。この記事では、WorkoutSessionに関して深堀りします。

https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/build_a_workout_app_for_apple_watch

https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/running_workout_sessions

サンプルコードを参考に進めます。主にWorkoutの状態を管理しているWorkoutManagerに着目して解説していきます。

セットアップ

セットアップ方法に関して以下にまとめていますので参考までにご覧ください。

https://zenn.dev/muranaka/articles/8d58f0065f19a7

権限リクエスト

WorkoutSessionを作成する前に、HealthKitを設定し、アプリが使用するヘルスケアデータの読み取りと共有の許可を要求する必要があります。

    // Request authorization to access HealthKit.
    func requestAuthorization() {
        // The quantity type to write to the health store.
        let typesToShare: Set = [
            HKQuantityType.workoutType()
        ]

        // The quantity types to read from the health store.
        let typesToRead: Set = [
            HKQuantityType.quantityType(forIdentifier: .heartRate)!,
            HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
            HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!,
            HKQuantityType.quantityType(forIdentifier: .distanceCycling)!,
            HKObjectType.activitySummaryType()
        ]

        // Request authorization for those quantity types.
        healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in
            // Handle error.
        }
    }

requestAuthorizationメソッドを用いて、ヘルスデータを扱う上での権限のリクエストを実行します。

  • toShare データの書き込みため
  • read データの読み込み

にヘルスデータタイプ(workout、心拍数、歩行距離etc)を指定してリクエストを行います。

Background Modesの設定

WorkoutSessionを用いるアプリはバックグラウンドで実行可能であるため、WatchKit App Extensionにバックグラウンドモード機能を追加する必要があります。

Workoutを開始する

HKWorkoutConfigurationの設定

HKWorkoutConfiguration()を生成して、運動の種類、屋内or屋外を設定する。

let configuration = HKWorkoutConfiguration()
configuration.activityType = workoutType
configuration.locationType = .outdoor

HKLiveWorkoutBuilderの生成

HKWorkoutConfiguration() を使用してHKWorkoutSessionを作成します。その後、HKWorkoutSessionからHKLiveWorkoutBuilderを取得します。

do {
    session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
    builder = session.associatedWorkoutBuilder()
} catch {
    // Handle failure here.
    return
}

HKWorkoutSession・HKLiveWorkoutBuilderについて

HKWorkoutSessionHKLiveWorkoutBuilderを用いて、Workoutの状態や心拍数のライブデータなどを取得します。

var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?

HKWorkoutSessionHKLiveWorkoutBuilderの役割は以下のとおりです。

HKWorkoutSession(Session):

  • ワークアウト全体の期間と状態を管理します。
  • ワークアウトの開始、一時停止、再開、終了を制御します。
  • イベントやエラーの通知します。

HKLiveWorkoutBuilder(Builder):

  • セッション中に収集されるデータの記録と管理します。
  • リアルタイムでのデータサンプルとイベントの収集します。

Delegateの設定

session.delegate = self
builder.delegate = self

を実装し、デリゲートメソッドを実装します。詳細に関しては後述いたします。

HKLiveWorkoutDataSourceの生成

HKLiveWorkoutDataSourceとは、WorkoutSessionからライブデータを提供するDataSourceです。HKLiveWorkoutDataSourceオブジェクトを作成し、builderに割り当てます。

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

WorkoutのSessionとBuilderを開始

SessionとBuilderを開始します。

// Start the workout session and begin data collection.
let startDate = Date()
session?.startActivity(with: startDate)
builder?.beginCollection(withStart: startDate) { (success, error) in
        // The workout has started.
}

Workoutの状態の変更

運動前・運動中・休憩中・終了かの状態をHKWorkoutSessionで管理しています。以下のメソッドでWorkoutの状態を操作することができます。

  • 一時停止
session?.pause()
  • 再開
session?.resume()
  • 終了
session?.end()

SessionとBuilderの終了

ワークアウトが終了した後に、Sessionを終了し、BuilderのendCollection(withEnd:completion:)finishWorkout(completion:)メソッドを呼び出します。

endCollection(withEnd:completion:)

  • ワークアウトの終了日時を設定し、ビルダーを無効化します。
  • 完了ハンドラーは、終了処理が成功したかどうかを示す success と、エラーが発生した場合の error を受け取ります。

finishWorkout(completion:)

  • ワークアウトとその関連データをHealthKitStoreに保存します。
  • 完了ハンドラーは、workout(HKWorkout) と、エラーが発生した場合の error を受け取ります。

以下、SessionとBuilderの終了の実装例です。

session.end()
builder.endCollection(withEnd: Date()) { (success, error) in
    
    guard success else {
        // Handle errors.
    }
    
    builder.finishWorkout { (workout, error) in
        
        guard workout != nil else {
            // Handle errors.
        }
        
        DispatchQueue.main.async() {
            // Update the user interface.
        }
    }
}

Workoutに関連したDelegate

HKWorkoutSessionDelegateHKLiveWorkoutBuilderDelegateが存在します。

  • HKWorkoutSessionDelegateは、Sessionの状態が変化したとき、またはSessionがエラーで失敗したときに更新を受け取ります。

  • HKLiveWorkoutBuilderDelegateは、Builderに新しいサンプルやイベントを追加したときに更新を受け取ります。

HKWorkoutSessionDelegateについて

Workoutの状態を通知するメソッド(必須)

必須のメソッドであるworkoutSession(_:didChangeTo:from:date:)です。Workoutの状態(HKWorkoutSessionState)が変更されたときに処理されるメソッドです。以下、表です。

引数項目 説明
workoutSession 変化したworkoutSession。
toState セッションの変更後の状態。HKWorkoutSessionState
fromState セッションの変更前の状態。HKWorkoutSessionState
date 状態の変更がいつ発生したかを示すDate。

HKWorkoutSessionStateは、WorkoutSessionの状態を表す列挙型です。以下、表です。

状態 説明
notStarted トレーニングセッションは開始されていません。
prepared セッションは準備ができていますが、まだ開始されていません。
running トレーニングセッションが実行中です。
paused トレーニングセッションが一時停止されました。
stopped セッションが停止しました。
ended トレーニングセッションが終了しました。

stoppedendedの違いは、

  • セッションを再開・再利用できない。
  • トレーニングデータを生成しなくなる。

ことは共通しています。異なる点として、stoppedはアプリをバックグラウンドで動作し続けることができますが、endedはバックグラウンドも停止されます。

WorkoutのSessionの失敗を通知するメソッド(必須)

必須のメソッドであるworkoutSession(_:didFailWithError:)があります。このメソッドはSessionの生成・状態管理が失敗した場合に通知されるメソッドです。以下、表です。

項目 説明
workoutSession 失敗したHKWorkoutSession
error 失敗をに関するErrorオブジェクト

HKLiveWorkoutBuilderDelegateについて

新しいヘルスケアデータが追加されたことを通知するメソッド(必須)

必須のメソッドであるworkoutBuilder(_:didCollectDataOf:)があります。このメソッドは新しいデータがビルダに追加されたことを通知するメソッドです。

引数名 説明
workoutBuilder HKLiveWorkoutBuilder イベントを収集したHKLiveWorkoutBuilder。ワークアウトデータの詳細を取得するために使用されます。
collectedTypes Set<HKSampleType> 収集されたサンプルデータ型の集合。HKSampleTypeの集合であり、特定のサンプルデータ型を示します。

使用例を以下に示します。

    func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
        for type in collectedTypes {
            guard let quantityType = type as? HKQuantityType else {
                return // Nothing to do.
            }

            let statistics = workoutBuilder.statistics(for: quantityType)

            // Update the published values.
            updateForStatistics(statistics)
        }
    }

処理を追っていきます。

for type in collectedTypes {
// 省略
}

collectedTypes内に存在する通知されたサンプルデータの集合を全てfor文で処理する。

let statistics = workoutBuilder.statistics(for: quantityType)

各データタイプを引数に設定して、workoutBuilderに存在するヘルスデータを取得する。

 // Update the published values.
updateForStatistics(statistics)

取得したデータを用いて、UIに関連するプロパティを更新する。

新しいイベントが追加されたことを通知するメソッド(必須)

必須のメソッドであるworkoutBuilderDidCollectEvent(_:)があります。新しいイベントがビルダに追加されたことを通知するメソッドです。

let lastEvent = workoutBuilder.workoutEvents.last

上記の実装でビルダからイベントを取得できます。イベントはHKWorkoutEventで表され、イベントを取得できます。以下、プロパティリストです。

変数名 説明
dateInterval DateInterval イベントの時間と期間
type HKWorkoutEventType ワークアウトイベントの種類
metadata [String : Any]? ワークアウトイベントに関連するメタデータ
  • dateIntervalHKWorkoutEventType.lap および HKWorkoutEventType.segmentのみ日時間隔を含むDateIntervalを取得できる。それ以外のEventに関してはイベントが発生した時点の示すdateInterval(日時間隔が0)の値を取得できます。

  • typeHKWorkoutEventTypeです。以下、表です。

イベント名 説明
pause ワークアウトが一時停止されたことを示す定数
resume ワークアウトが再開されたことを示す定数
motionPaused システムがセッションを自動的に一時停止したことを示す定数
motionResumed システムがセッションを自動的に再開したことを示す定数
pauseOrResumeRequest ユーザーが一時停止または再開を要求したことを示す定数
lap ラップを示す定数
segment ワークアウト中の注目すべき期間を示す定数
marker ワークアウト中の注目すべきポイントを示す定数

例として、HKWorkoutEventType.lapイベントが発生したら、ラップ数のUIを更新するなどが挙げられます。

その他

  • iOSとWatchOSを同期させてWorkoutを管理
  • Workout中に生じたクラッシュからのリカバリー方法

上記に関して公式に記載されていますが、今回の記事では省きます。以下の公式に記載ありますので、参考までにご覧ください。

https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/running_workout_sessions#2962487

WorkoutSessionに関連した主な実装は以上になります。

最後に

間違い・気になる部分がありましたら、コメントいただけると大変うれしいです。
良かったと思ったら記事へのいいねXのフォローをよろしくお願いいたします。

https://sites.google.com/view/muranakar

個人でアプリを作成しています。良かったら覗いてみてください。

Discussion