Xamarin の watchOS ワークアウト アプリ

この記事では、Apple が watchOS 3 のワークアウト アプリに対して行った機能強化と、それらを Xamarin で実装する方法について説明します。

watchOS 3 の新機能として、ワークアウト関連のアプリが、Apple Watch のバックグラウンドで実行できるようになり、HealthKit データへのアクセスも可能になりました。 親 iOS 10 ベースのアプリには、ユーザーの介入なしに watchOS 3 ベースのアプリを起動する機能もあります。

次のトピックで、詳しく説明します。

ワークアウト アプリについて

フィットネスまたはワークアウト アプリのユーザーは非常に熱心であり、1 日のうち数時間を健康とフィットネスの目標のために費やしています。 このため、アプリには、応答性と使いやすさ、正確なデータ収集と表示、そして Apple Health とのシームレスな統合を期待しています。

適切に設計されたフィットネスまたはワークアウト アプリでは、ユーザーが自身のフィットネス目標を達成するためのアクティビティをグラフにすることができます。 Apple Watch を使用すると、フィットネスまたはワークアウト アプリで、心拍数、カロリー消費、アクティビティ検出にすぐにアクセスできます。

フィットネスおよびワークアウト アプリの例

watchOS 3 の新機能 "バックグラウンド実行" により、ワークアウト関連のアプリを Apple Watch のバックグラウンドで実行できるようになり、HealthKit データへのアクセスも可能になりました。

このドキュメントでは、バックグラウンド実行機能、およびワークアウト アプリのライフサイクルについて紹介し、ワークアウト アプリが Apple Watch のユーザーの "アクティビティ リング" にどのように貢献するかを説明します。

ワークアウト セッションについて

すべてのワークアウト アプリの中心となるのが "ワークアウト セッション"(HKWorkoutSession) であり、これはユーザーが開始および停止することができます。 ワークアウト セッション API は簡単に実装でき、ワークアウト アプリに次のようなメリットを提供します。

  • アクティビティの種類に基づいてモーションとカロリー消費を検出する。
  • ユーザーのアクティビティ リングに自動的に貢献する。
  • セッション中、ユーザーがデバイスのスリープ状態を解除する (手首を上げるか Apple Watch を操作する) たびに、アプリが自動的に表示される。

バックグラウンド実行について

前述のように、watchOS 3 では、ワークアウト アプリをバックグラウンドで実行されるように設定できます。 バックグラウンド実行を使用することで、ワークアウト アプリはバックグラウンドで動作しながら、Apple Watch のセンサーからのデータを処理できます。 たとえば、アプリは画面に表示されなくなっても、ユーザーの心拍数を引き続き監視できます。

また、バックグラウンド実行では、アクティブなワークアウト セッション中いつでも、ライブ フィードバックをユーザーに表示できます。たとえば、触感的なアラートを送信して現在の進行状況をユーザーに通知します。

さらに、バックグラウンド実行により、アプリのユーザー インターフェイスをすばやく更新できるため、ユーザーは Apple Watch を見た瞬間に最新のデータを確認することができます。

バックグラウンド実行を使用しているウォッチ アプリでは、Apple Watch でハイ パフォーマンスを確保するために、バックグラウンド処理の量を制限してバッテリーを節約する必要があります。 過剰な CPU をバックグラウンドで使用しているアプリは、watchOS によって一時停止される可能性があります。

バックグラウンド実行の有効化

バックグラウンド実行を有効にするには、次の操作を行います。

  1. ソリューション エクスプローラーで、Watch Extension のコンパニオン iPhone アプリの Info.plist ファイルをダブルクリックして、編集用に開きます。

  2. [ソース] ビューに切り替えます。

    ソース ビュー

  3. WKBackgroundModes という新しいキーを追加し、Array に設定します。

    WKBackgroundModes という新しいキーを追加する

  4. String、値が workout-processing の配列に新しい項目を追加します。

    String 型と workout-processing の値を含む新しい項目を配列に追加する

  5. 変更をファイルに保存します。

ワークアウト セッションの開始

ワークアウト セッションを開始するには、主に次の 3 つの手順を実行します。

ワークアウト セッションを開始するには、主に次の 3 つの手順を実行します

  1. アプリが HealthKit 内のデータにアクセスできるよう承認を要求する必要があります。
  2. 開始するワークアウトの種類に対して、ワークアウト構成オブジェクトを作成します。
  3. 新しく作成したワークアウト構成を使用して、ワークアウト セッションを作成して開始します。

承認の要求

アプリがユーザーの HealthKit データにアクセスできるようにするには、ユーザーに承認を要求し、ユーザーの承認を受けておく必要があります。 ワークアウト アプリの性質に応じて、次の種類の要求が行われる場合があります。

  • データの書き込みの承認:
    • ワークアウト
  • データの読み取りの承認:
    • エネルギー消費
    • Distance
    • 心拍数

アプリが承認を要求できるようにするには、HealthKit にアクセスできるように構成しておく必要があります。

次の操作を行います。

  1. ソリューション エクスプローラーEntitlements.plist ファイルをダブルクリックして、編集用に開きます。

  2. 一番下までスクロールし、[HealthKit を有効にする] をオンにします。

    [HealthKit を有効にする] をオンにする

  3. 変更をファイルに保存します。

  4. HealthKit の概要に関する記事の「明示的なアプリ ID とプロビジョニング プロファイル」セクションと「アプリ ID とプロビジョニング プロファイルと Xamarin.iOS アプリの関連付け」セクションの手順に従って、アプリを正しくプロビジョニングします。

  5. 最後に、HealthKit の概要に関する記事の「プログラミング ヘルス キット」セクションと「ユーザーにアクセス許可を要求する」セクションの手順を使用して、ユーザーの HealthKit データストアにアクセスするための承認を要求します。

ワークアウト構成の設定

ワークアウト セッションは、ワークアウト構成オブジェクト (HKWorkoutConfiguration) を使用して作成されます。このオブジェクトでは、ワークアウトの種類 (HKWorkoutActivityType.Running など) とワークアウトの場所 (HKWorkoutSessionLocationType.Outdoor など) が指定されます。

using HealthKit;
...

// Create a workout configuration
var configuration = new HKWorkoutConfiguration () {
  ActivityType = HKWorkoutActivityType.Running,
  LocationType = HKWorkoutSessionLocationType.Outdoor
};

ワークアウト セッション デリゲートの作成

ワークアウト セッション中に発生する可能性のあるイベントを処理するには、アプリでワークアウト セッション デリゲート インスタンスを作成する必要があります。 新しいクラスをプロジェクトに追加し、そのベースを HKWorkoutSessionDelegate クラスにします。 屋外ランニングの例では、次のようになります。

using System;
using Foundation;
using WatchKit;
using HealthKit;

namespace MonkeyWorkout.MWWatchExtension
{
  public class OutdoorRunDelegate : HKWorkoutSessionDelegate
  {
    #region Computed Properties
    public HKHealthStore HealthStore { get; private set; }
    public HKWorkoutSession WorkoutSession { get; private set;}
    #endregion

    #region Constructors
    public OutdoorRunDelegate (HKHealthStore healthStore, HKWorkoutSession workoutSession)
    {
      // Initialize
      this.HealthStore = healthStore;
      this.WorkoutSession = workoutSession;

      // Attach this delegate to the session
      workoutSession.Delegate = this;
    }
    #endregion

    #region Override Methods
    public override void DidFail (HKWorkoutSession workoutSession, NSError error)
    {
      // Handle workout session failing
      RaiseFailed ();
    }

    public override void DidChangeToState (HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date)
    {
      // Take action based on the change in state
      switch (toState) {
      case HKWorkoutSessionState.NotStarted:
        break;
      case HKWorkoutSessionState.Paused:
        RaisePaused ();
        break;
      case HKWorkoutSessionState.Running:
        RaiseRunning ();
        break;
      case HKWorkoutSessionState.Ended:
        RaiseEnded ();
        break;
      }

    }

    public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
    {
      base.DidGenerateEvent (workoutSession, @event);
    }
    #endregion

    #region Events
    public delegate void OutdoorRunEventDelegate ();

    public event OutdoorRunEventDelegate Failed;
    internal void RaiseFailed ()
    {
      if (this.Failed != null) this.Failed ();
    }

    public event OutdoorRunEventDelegate Paused;
    internal void RaisePaused ()
    {
      if (this.Paused != null) this.Paused ();
    }

    public event OutdoorRunEventDelegate Running;
    internal void RaiseRunning ()
    {
      if (this.Running != null) this.Running ();
    }

    public event OutdoorRunEventDelegate Ended;
    internal void RaiseEnded ()
    {
      if (this.Ended != null) this.Ended ();
    }
    #endregion
  }
}

このクラスにより、ワークアウト セッションの状態の変化した場合 (DidChangeToState)、およびワークアウト セッションが失敗した場合 (DidFail) に発生するイベントがいくつか作成されます。

ワークアウト セッションの作成

上記で作成されたワークアウト構成およびワークアウト セッション デリゲートを使用して、新しいワークアウト セッションを作成し、ユーザーの既定の HealthKit ストアに対して開始します。

using HealthKit;
...

#region Computed Properties
public HKHealthStore HealthStore { get; set;} = new HKHealthStore ();
public OutdoorRunDelegate RunDelegate { get; set; }
#endregion
...

private void StartOutdoorRun ()
{
  // Create a workout configuration
  var configuration = new HKWorkoutConfiguration () {
    ActivityType = HKWorkoutActivityType.Running,
    LocationType = HKWorkoutSessionLocationType.Outdoor
  };

  // Create workout session
  // Start workout session
  NSError error = null;
  var workoutSession = new HKWorkoutSession (configuration, out error);

  // Successful?
  if (error != null) {
    // Report error to user and return
    return;
  }

  // Create workout session delegate and wire-up events
  RunDelegate = new OutdoorRunDelegate (HealthStore, workoutSession);

  RunDelegate.Failed += () => {
    // Handle the session failing
  };

  RunDelegate.Paused += () => {
    // Handle the session being paused
  };

  RunDelegate.Running += () => {
    // Handle the session running
  };

  RunDelegate.Ended += () => {
    // Handle the session ending
  };

  // Start session
  HealthStore.StartWorkoutSession (workoutSession);
}

アプリがこのワークアウト セッションを開始し、ユーザーがウォッチの文字盤に戻ると、小さな緑色の "ランニングしている人" のアイコンが文字盤に表示されます。

文字盤の上部に表示される小さな緑色のランニングしている人のアイコン

ユーザーがこのアイコンをタップすると、アプリに戻ります。

データ収集とコントロール

ワークアウト セッションが構成され、開始されたら、アプリはセッションに関するデータ (ユーザーの心拍数など) を収集し、セッションの状態を制御する必要があります。

データ収集とコントロールの図

  1. サンプルの監視 - アプリは、HealthKit から情報を取得する必要があります。これが処理され、ユーザーに表示されます。
  2. イベントの監視 - アプリは、HealthKit またはアプリの UI によって生成されたイベント (ユーザーによるワークアウトの一時停止など) に応答する必要があります。
  3. 実行状態の開始 - セッションが開始され、実行中です。
  4. 一時停止状態の開始 - ユーザーは現在のワークアウト セッションを一時停止しました。これは後で再開できます。 ユーザーは 1 回のワークアウト セッションで、実行状態と一時停止状態を複数回切り替えることができます。
  5. ワークアウトセッションの終了 - ユーザーはいつでもワークアウト セッションを終了できます。計測ワークアウト (2 マイル ランニングなど) の場合は、期限切れで終了する場合もあります。

最後の手順で、ワークアウト セッションの結果を、ユーザーの HealthKit データストアに保存します。

HealthKit サンプルの監視

アプリでは、心拍数やアクティブなエネルギー消費など、興味のある HealthKit データ ポイントごとに "アンカー オブジェクト クエリ" を開く必要があります。 監視対象のデータ ポイントごとに、更新ハンドラーを作成する必要があり、これにより新しいデータがアプリに送信されたときにキャプチャされます。

アプリは、これらのデータ ポイントから、総ランニング距離などの合計にデータを蓄積し、必要に応じてユーザー インターフェイスを更新できます。 さらに、ランニングでさらに 1 マイルを完走するなど、固有ゴールやアチーブメントに達したときにユーザーに通知することもできます。

次のサンプル コードを見てみましょう。

private void ObserveHealthKitSamples ()
{
  // Get the starting date of the required samples
  var datePredicate = HKQuery.GetPredicateForSamples (WorkoutSession.StartDate, null, HKQueryOptions.StrictStartDate);

  // Get data from the local device
  var devices = new NSSet<HKDevice> (new HKDevice [] { HKDevice.LocalDevice });
  var devicePredicate = HKQuery.GetPredicateForObjectsFromDevices (devices);

  // Assemble compound predicate
  var queryPredicate = NSCompoundPredicate.CreateAndPredicate (new NSPredicate [] { datePredicate, devicePredicate });

  // Get ActiveEnergyBurned
  var queryActiveEnergyBurned = new HKAnchoredObjectQuery (HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned), queryPredicate, null, HKSampleQuery.NoLimit, (query, addedObjects, deletedObjects, newAnchor, error) => {
    // Valid?
    if (error == null) {
      // Yes, process all returned samples
      foreach (HKSample sample in addedObjects) {
        var quantitySample = sample as HKQuantitySample;
        ActiveEnergyBurned += quantitySample.Quantity.GetDoubleValue (HKUnit.Joule);
      }

      // Update User Interface
      ...
    }
  });

  // Start Query
  HealthStore.ExecuteQuery (queryActiveEnergyBurned);

}

これは、GetPredicateForSamples メソッドを使用して、データ取得を開始する日付を設定する述語を作成します。 GetPredicateForObjectsFromDevices メソッドを使用して、HealthKit 情報をプルする一連のデバイスのセットを作成します。この場合は、ローカル Apple Watch のみ (HKDevice.LocalDevice) です。 2 つの述語は、CreateAndPredicate メソッドを使用して複合述語 (NSCompoundPredicate) に結合されます。

新しい HKAnchoredObjectQuery が、目的のデータ ポイントに対して作成され (この場合は、アクティブなエネルギー消費データ ポイントに対する HKQuantityTypeIdentifier.ActiveEnergyBurned)、返されるデータ量に制限はなく (HKSampleQuery.NoLimit)、HealthKit からアプリに返されるデータを処理するために更新ハンドラーが定義されます。

更新ハンドラーは、指定されたデータ ポイントに対して、新しいデータがアプリに配信されるたびに呼び出されます。 エラーが返されなければ、アプリはデータを安全に読み取り、必要な計算を行い、必要に応じて UI を更新できます。

このコードは、addedObjects 配列で返されたすべてのサンプル (HKSample) をループし、数量サンプル (HKQuantitySample) にキャストします。 その後、サンプルの double 値をジュールとして取得し (HKUnit.Joule)、それを、ワークアウトで消費されたアクティブなエネルギーの合計に蓄積し、ユーザー インターフェイスを更新します。

目標達成の通知

前述のように、ユーザーがワークアウト アプリで目標を達成したとき (たとえば、ランニングの最初の 1 マイルを完走したときに)、そのユーザーに、Taptic Engine を介して触感フィードバックを送信することができます。 ユーザーは、そのフィードバックを表示したイベントを、手首を上げて確認する可能性が高いため、アプリはこの時点で UI も更新する必要があります。

触感フィードバックを再生するには、次のコードを使用します。

// Play haptic feedback
WKInterfaceDevice.CurrentDevice.PlayHaptic (WKHapticType.Notification);

イベントの監視

イベントはアプリが使用するタイムスタンプであり、これによりユーザーのワークアウト中にアプリが特定のポイントを強調表示できます。 イベントの中には、アプリによって直接作成され、ワークアウトに保存されるものや、HealthKit によって自動的に作成されるものがあります。

HealthKit によって作成されたイベントを観察するために、アプリは HKWorkoutSessionDelegateDidGenerateEvent メソッドをオーバーライドします。

using System.Collections.Generic;
...

public List<HKWorkoutEvent> WorkoutEvents { get; set; } = new List<HKWorkoutEvent> ();
...

public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
{
  base.DidGenerateEvent (workoutSession, @event);

  // Save HealthKit generated event
  WorkoutEvents.Add (@event);

  // Take action based on the type of event
  switch (@event.Type) {
  case HKWorkoutEventType.Lap:
    break;
  case HKWorkoutEventType.Marker:
    break;
  case HKWorkoutEventType.MotionPaused:
    break;
  case HKWorkoutEventType.MotionResumed:
    break;
  case HKWorkoutEventType.Pause:
    break;
  case HKWorkoutEventType.Resume:
    break;
  }
}

Apple は、watchOS 3 では、次のイベントの種類が新しく追加されました。

  • HKWorkoutEventType.Lap - ワークアウトを等距離に分割するイベント。 ランニング中のトラック 1 周など。
  • HKWorkoutEventType.Marker - ワークアウト内の任意の POI。 屋外ランニング ルート上の特定のポイントに到達する、など。

このような新しい種類はアプリによって作成され、後でグラフや統計を作成するときに使用できるようワークアウトに保存できます。

マーカー イベントを作成するには、次の操作を行います。

using System.Collections.Generic;
...

public float MilesRun { get; set; }
public List<HKWorkoutEvent> WorkoutEvents { get; set; } = new List<HKWorkoutEvent> ();
...

public void ReachedNextMile ()
{
  // Create and save marker event
  var markerEvent = HKWorkoutEvent.Create (HKWorkoutEventType.Marker, NSDate.Now);
  WorkoutEvents.Add (markerEvent);

  // Notify user
  NotifyUserOfReachedMileGoal (++MilesRun);
}

このコードでは、マーカー イベント (HKWorkoutEvent) の新しいインスタンスを作成し、それをイベントのプライベート コレクションに保存し (これは後でワークアウトに セッションに書き込まれます)、触感によってそのイベントをユーザーに通知します。

ワークアウトの一時停止と再開

ユーザーはワークアウト セッションをいつでも一時停止し、後で再開することができます。 たとえば、重要な電話を受けるために室内ランニングを一時停止し、電話が終わった後に再開する場合があります。

ユーザーがアクティビティを中断している間、Apple Watch が電力とデータ領域の両方を節約できるように、アプリの UI で (HealthKit を呼び出すことで) ワークアウトを一時停止および再開できる必要があります。 また、アプリは、ワークアウト セッションが一時停止状態のときに受信する可能性がある、すべての新しいデータ ポイントを無視する必要があります。

HealthKit は、一時停止イベントと再開イベントを生成することで、一時停止と再開の呼び出しに応答します。 ワークアウト セッションが一時停止されている間、セッションが再開されるまで、HealthKit によって新しいイベントやデータがアプリに送信されることはありません。

ワークアウト セッションを一時停止して再開するには、次のコードを使用します。

public HKHealthStore HealthStore { get; set;} = new HKHealthStore ();
public HKWorkoutSession WorkoutSession { get; set;}
...

public void PauseWorkout ()
{
  // Pause the current workout
  HealthStore.PauseWorkoutSession (WorkoutSession);
}

public void ResumeWorkout ()
{
  // Pause the current workout
  HealthStore.ResumeWorkoutSession (WorkoutSession);
}

HealthKit から生成される一時停止イベントと再開イベントは、HKWorkoutSessionDelegateDidGenerateEvent メソッドをオーバーライドすることで処理できます。

public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
{
  base.DidGenerateEvent (workoutSession, @event);

  // Take action based on the type of event
  switch (@event.Type) {
  case HKWorkoutEventType.Pause:
    break;
  case HKWorkoutEventType.Resume:
    break;
  }
}

モーション イベント

また、watchOS 3 の新機能として、モーション一時停止 (HKWorkoutEventType.MotionPaused) イベントとモーション再開 (HKWorkoutEventType.MotionResumed) イベントがあります。 これらのイベントは、ランニング ワークアウト中、ユーザーが動き出したり止まったりしたときに、HealthKit によって自動的に発生します。

モーション一時停止イベントを受信したアプリは、ユーザーが動き始めて、モーション再開イベントを受信するまで、データの収集を停止する必要があります。 アプリはモーション一時停止イベントに応答するために、ワークアウト セッションを一時停止すべきではありません。

重要

モーション一時停止イベントとモーション再開イベントは、RunningWorkout アクティビティの種類 (HKWorkoutActivityType.Running) でのみサポートされています。

これらのイベントも、HKWorkoutSessionDelegateDidGenerateEvent メソッドをオーバーライドすることで処理できます。

public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
{
  base.DidGenerateEvent (workoutSession, @event);

  // Take action based on the type of event
  switch (@event.Type) {
  case HKWorkoutEventType.MotionPaused:
    break;
  case HKWorkoutEventType.MotionResumed:
    break;
  }
}

ワークアウト セッションの終了と保存

ユーザーがワークアウトを完了したら、アプリは現在のワークアウト セッションを終了し、HealthKit データベースに保存する必要があります。 HealthKit に保存されたワークアウトは、ワークアウト アクティビティ リストに自動的に表示されます。

iOS 10 の新機能として、これにはユーザーの iPhone のワークアウト アクティビティ リストも含まれます。 このため、Apple Watch が近くになくても、ワークアウトは電話に表示されます。

エネルギー サンプルを含むワークアウトにより、アクティビティ アプリのユーザーのムーブ リングが更新され、サード パーテのアプリがユーザーの 1 日のムーブの目標に貢献できるようになりました。

ワークアウト セッションを終了して保存するには、次の手順が必要です。

ワークアウト セッションの終了と保存の図

  1. まず、アプリはワークアウト セッションを終了する必要があります。
  2. ワークアウト セッションは HealthKit に保存されます。
  3. 保存したトレーニング セッションにサンプル (エネルギー消費や距離など) を追加します。

セッションの終了

ワークアウト セッションを終了するには、HKHealthStoreEndWorkoutSession メソッドを呼び出して、HKWorkoutSession を渡します。

public HKHealthStore HealthStore { get; private set; }
public HKWorkoutSession WorkoutSession { get; private set;}
...

public void EndOutdoorRun ()
{
  // End the current workout session
  HealthStore.EndWorkoutSession (WorkoutSession);
}

これにより、デバイス センサーが通常モードにリセットされます。 HealthKit は、ワークアウトの終了を完了すると、HKWorkoutSessionDelegateDidChangeToState メソッドへのコールバックを受け取ります。

public override void DidChangeToState (HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date)
{
  // Take action based on the change in state
  switch (toState) {
  ...
  case HKWorkoutSessionState.Ended:
    StopObservingHealthKitSamples ();
    RaiseEnded ();
    break;
  }

}

セッションの保存

ワークアウト セッションを終了したアプリは、ワークアウト (HKWorkout) を作成し、それを (イベントとともに) HealthKit データ ストア (HKHealthStore) に保存する必要があります。

public HKHealthStore HealthStore { get; private set; }
public HKWorkoutSession WorkoutSession { get; private set;}
public float MilesRun { get; set; }
public double ActiveEnergyBurned { get; set;}
public List<HKWorkoutEvent> WorkoutEvents { get; set; } = new List<HKWorkoutEvent> ();
...

private void SaveWorkoutSession ()
{
  // Build required workout quantities
  var energyBurned = HKQuantity.FromQuantity (HKUnit.Joule, ActiveEnergyBurned);
  var distance = HKQuantity.FromQuantity (HKUnit.Mile, MilesRun);

  // Create any required metadata
  var metadata = new NSMutableDictionary ();
  metadata.Add (new NSString ("HKMetadataKeyIndoorWorkout"), new NSString ("NO"));

  // Create workout
  var workout = HKWorkout.Create (HKWorkoutActivityType.Running,
                                  WorkoutSession.StartDate,
                                  NSDate.Now,
                                  WorkoutEvents.ToArray (),
                                  energyBurned,
                                  distance,
                                  metadata);

  // Save to HealthKit
  HealthStore.SaveObject (workout, (successful, error) => {
    // Handle any errors
    if (error == null) {
      // Was the save successful
      if (successful) {

      }
    } else {
      // Report error
    }
  });

}

このコードでは、ワークアウトに必要な総エネルギー消費量と距離を HKQuantity オブジェクトとして作成します。 ワークアウトを定義するメタデータの辞書が作成され、ワークアウトの場所が指定されます。

metadata.Add (new NSString ("HKMetadataKeyIndoorWorkout"), new NSString ("NO"));

新しい HKWorkout オブジェクトは、HKWorkoutSession と同じ HKWorkoutActivityType、開始日と終了日、イベントの一覧 (上記のセクションから蓄積)、エネルギー消費、合計距離、メタデータ辞書で作成されます。 このオブジェクトは正常性ストアに保存され、すべてのエラーが処理されます。

サンプルの追加

アプリが一連のサンプルをワークアウトに保存すると、HealthKit により、サンプルとワークアウト自体の間の接続が生成されるため、アプリは、特定のワークアウトに関連付けられているすべてのサンプルについて、後で HealthKit にクエリを実行することができます。 アプリは、この情報を使用してワークアウト データからグラフを生成し、それをワークアウト タイムラインに対してプロットできます。

アプリがアクティビティ アプリのムーブ リングに貢献するためには、保存されたワークアウトにエネルギー サンプルを含める必要があります。 さらに、距離とエネルギーの合計は、アプリによって保存済みワークアウトに関連付けられているすべてのサンプルの合計と一致する必要があります。

保存されたワークアウトにサンプルを追加するには、次の操作を行います。

using System.Collections.Generic;
using WatchKit;
using HealthKit;
...

public HKHealthStore HealthStore { get; private set; }
public List<HKSample> WorkoutSamples { get; set; } = new List<HKSample> ();
...

private void SaveWorkoutSamples (HKWorkout workout)
{
  // Add samples to saved workout
  HealthStore.AddSamples (WorkoutSamples.ToArray (), workout, (success, error) => {
    // Handle any errors
    if (error == null) {
      // Was the save successful
      if (success) {

      }
    } else {
      // Report error
    }
  });
}

アプリは、必要に応じて、サンプルの小さなサブセットまたは 1 つのメガ サンプル (ワークアウトの範囲全体にまたがります) を計算し、作成することができます。これが、保存されたワークアウトに関連付けられます。

ワークアウトと iOS 10

すべての watchOS 3 ワークアウト アプリには親 iOS 10 ベースのワークアウト アプリがあり、iOS 10 の新機能であるこの iOS アプリを使用して、ワークアウトを開始できます。これにより Apple Watch が (ユーザーの介入なしで) ワークアウト モードになり、watchOS アプリが、バックグラウンド実行モードで実行されます (詳細については、上記の「バックグラウンド実行について」を参照してください)。

watchOS アプリの実行中は、親 iOS アプリとのメッセージングと通信に WatchConnectivity を使用できます。

このプロセスのしくみを確認してみましょう。

iPhone と Apple Watch の通信の図

  1. iPhone アプリは HKWorkoutConfiguration オブジェクトを作成し、ワークアウトの種類と場所を設定します。
  2. HKWorkoutConfiguration オブジェクトは Apple Watch バージョンのアプリに送信され、システムによって起動されます (まだ実行されていない場合)。
  3. watchOS 3 アプリは、渡されたワークアウト構成を使用して、新しいワークアウト セッション (HKWorkoutSession) を開始します。

重要

親 iPhone アプリが Apple Watch でワークアウトを開始するには、watchOS 3 アプリでバックグラウンド実行が有効になっている必要があります。 詳細については、上記の「バックグラウンド実行の有効化」を参照してください。

このプロセスは、watchOS 3 アプリで直接ワークアウト セッションを開始するプロセスとよく似ています。 iPhone では、次のコードを使用します。

using System;
using HealthKit;
using WatchConnectivity;
...

#region Computed Properties
public HKHealthStore HealthStore { get; set; } = new HKHealthStore ();
public WCSession ConnectivitySession { get; set; } = WCSession.DefaultSession;
#endregion
...

private void StartOutdoorRun ()
{
  // Can the app communicate with the watchOS version of the app?
  if (ConnectivitySession.ActivationState == WCSessionActivationState.Activated && ConnectivitySession.WatchAppInstalled) {
    // Create a workout configuration
    var configuration = new HKWorkoutConfiguration () {
      ActivityType = HKWorkoutActivityType.Running,
      LocationType = HKWorkoutSessionLocationType.Outdoor
    };

    // Start watch app
    HealthStore.StartWatchApp (configuration, (success, error) => {
      // Handle any errors
      if (error == null) {
        // Was the save successful
        if (success) {
          ...
        }
      } else {
        // Report error
        ...
      }
    });
  }
}

このコードにより、アプリの watchOS バージョンがインストールされます。これには iPhone バージョンが最初に接続できます。

if (ConnectivitySession.ActivationState == WCSessionActivationState.Activated && ConnectivitySession.WatchAppInstalled) {
  ...
}

その後、通常どおりに HKWorkoutConfiguration を作成し、HKHealthStoreStartWatchApp メソッドを使用して、それを Apple Watch に送信し、アプリとワークアウト セッションを開始します。

また、watch OS アプリでは、WKExtensionDelegate で次のコードを使用します。

using WatchKit;
using HealthKit;
...

#region Computed Properties
public HKHealthStore HealthStore { get; set;} = new HKHealthStore ();
public OutdoorRunDelegate RunDelegate { get; set; }
#endregion
...

public override void HandleWorkoutConfiguration (HKWorkoutConfiguration workoutConfiguration)
{
  // Create workout session
  // Start workout session
  NSError error = null;
  var workoutSession = new HKWorkoutSession (workoutConfiguration, out error);

  // Successful?
  if (error != null) {
    // Report error to user and return
    return;
  }

  // Create workout session delegate and wire-up events
  RunDelegate = new OutdoorRunDelegate (HealthStore, workoutSession);

  RunDelegate.Failed += () => {
    // Handle the session failing
  };

  RunDelegate.Paused += () => {
    // Handle the session being paused
  };

  RunDelegate.Running += () => {
    // Handle the session running
  };

  RunDelegate.Ended += () => {
    // Handle the session ending
  };

  // Start session
  HealthStore.StartWorkoutSession (workoutSession);
}

HKWorkoutConfiguration を受け取り、新しい HKWorkoutSession を作成して、カスタム HKWorkoutSessionDelegate のインスタンスをアタッチします。 ワークアウト セッションは、ユーザーの HealthKit 正常性ストアに対して開始されます。

すべてのピースをまとめる

このドキュメントに記載されているすべての情報を取得すると、watchOS 3 ベースのワークアウト アプリとその親 iOS 10 ベースのワークアウト アプリには、次が含まれる場合があります。

  1. iOS 10 ViewController.cs - Apple Watch でウォッチ接続セッションとワークアウトの開始を処理します。
  2. watchOS 3 ExtensionDelegate.cs - ワークアウト アプリの watchOS 3 バージョンを処理します。
  3. watchOS 3 OutdoorRunDelegate.cs - ワークアウトのイベントを処理するカスタム HKWorkoutSessionDelegate

重要

次のセクションに示すコードには、watchOS 3 のワークアウト アプリに提供される、新しい強化機能を実装するために必要な部分のみが含まれています。 UI を表示および更新するサポート コードとコードがすべて含まれているわけではありませんが、これは他の watchOS ドキュメントに従って簡単に作成できます。

ViewController.cs

ワークアウト アプリの親 iOS 10 バージョンの ViewController.cs ファイルには、次のコードが含まれます。

using System;
using HealthKit;
using UIKit;
using WatchConnectivity;

namespace MonkeyWorkout
{
  public partial class ViewController : UIViewController
  {
    #region Computed Properties
    public HKHealthStore HealthStore { get; set; } = new HKHealthStore ();
    public WCSession ConnectivitySession { get; set; } = WCSession.DefaultSession;
    #endregion

    #region Constructors
    protected ViewController (IntPtr handle) : base (handle)
    {
      // Note: this .ctor should not contain any initialization logic.
    }
    #endregion

    #region Private Methods
    private void InitializeWatchConnectivity ()
    {
      // Is Watch Connectivity supported?
      if (!WCSession.IsSupported) {
        // No, abort
        return;
      }

      // Is the session already active?
      if (ConnectivitySession.ActivationState != WCSessionActivationState.Activated) {
        // No, start session
        ConnectivitySession.ActivateSession ();
      }
    }

    private void StartOutdoorRun ()
    {
      // Can the app communicate with the watchOS version of the app?
      if (ConnectivitySession.ActivationState == WCSessionActivationState.Activated && ConnectivitySession.WatchAppInstalled) {
        // Create a workout configuration
        var configuration = new HKWorkoutConfiguration () {
          ActivityType = HKWorkoutActivityType.Running,
          LocationType = HKWorkoutSessionLocationType.Outdoor
        };

        // Start watch app
        HealthStore.StartWatchApp (configuration, (success, error) => {
          // Handle any errors
          if (error == null) {
            // Was the save successful
            if (success) {
              ...
            }
          } else {
            // Report error
            ...
          }
        });
      }
    }
    #endregion

    #region Override Methods
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();

      // Start Watch Connectivity
      InitializeWatchConnectivity ();
    }
    #endregion
  }
}

ExtensionDelegate.cs

ワークアウト アプリの watchOS 3 バージョンの ExtensionDelegate.cs ファイルには、次のコードが含まれます。

using System;
using Foundation;
using WatchKit;
using HealthKit;

namespace MonkeyWorkout.MWWatchExtension
{
  public class ExtensionDelegate : WKExtensionDelegate
  {
    #region Computed Properties
    public HKHealthStore HealthStore { get; set;} = new HKHealthStore ();
    public OutdoorRunDelegate RunDelegate { get; set; }
    #endregion

    #region Constructors
    public ExtensionDelegate ()
    {

    }
    #endregion

    #region Private Methods
    private void StartWorkoutSession (HKWorkoutConfiguration workoutConfiguration)
    {
      // Create workout session
      // Start workout session
      NSError error = null;
      var workoutSession = new HKWorkoutSession (workoutConfiguration, out error);

      // Successful?
      if (error != null) {
        // Report error to user and return
        return;
      }

      // Create workout session delegate and wire-up events
      RunDelegate = new OutdoorRunDelegate (HealthStore, workoutSession);

      RunDelegate.Failed += () => {
        // Handle the session failing
        ...
      };

      RunDelegate.Paused += () => {
        // Handle the session being paused
        ...
      };

      RunDelegate.Running += () => {
        // Handle the session running
        ...
      };

      RunDelegate.Ended += () => {
        // Handle the session ending
        ...
      };

      RunDelegate.ReachedMileGoal += (miles) => {
        // Handle the reaching a session goal
        ...
      };

      RunDelegate.HealthKitSamplesUpdated += () => {
        // Update UI as required
        ...
      };

      // Start session
      HealthStore.StartWorkoutSession (workoutSession);
    }

    private void StartOutdoorRun ()
    {
      // Create a workout configuration
      var workoutConfiguration = new HKWorkoutConfiguration () {
        ActivityType = HKWorkoutActivityType.Running,
        LocationType = HKWorkoutSessionLocationType.Outdoor
      };

      // Start the session
      StartWorkoutSession (workoutConfiguration);
    }
    #endregion

    #region Override Methods
    public override void HandleWorkoutConfiguration (HKWorkoutConfiguration workoutConfiguration)
    {
      // Start the session
      StartWorkoutSession (workoutConfiguration);
    }
    #endregion
  }
}

OutdoorRunDelegate.cs

ワークアウト アプリの watchOS 3 バージョンの OutdoorRunDelegate.cs ファイルには、次のコードが含まれます。

using System;
using System.Collections.Generic;
using Foundation;
using WatchKit;
using HealthKit;

namespace MonkeyWorkout.MWWatchExtension
{
  public class OutdoorRunDelegate : HKWorkoutSessionDelegate
  {
    #region Private Variables
    private HKAnchoredObjectQuery QueryActiveEnergyBurned;
    #endregion

    #region Computed Properties
    public HKHealthStore HealthStore { get; private set; }
    public HKWorkoutSession WorkoutSession { get; private set;}
    public float MilesRun { get; set; }
    public double ActiveEnergyBurned { get; set;}
    public List<HKWorkoutEvent> WorkoutEvents { get; set; } = new List<HKWorkoutEvent> ();
    public List<HKSample> WorkoutSamples { get; set; } = new List<HKSample> ();
    #endregion

    #region Constructors
    public OutdoorRunDelegate (HKHealthStore healthStore, HKWorkoutSession workoutSession)
    {
      // Initialize
      this.HealthStore = healthStore;
      this.WorkoutSession = workoutSession;

      // Attach this delegate to the session
      workoutSession.Delegate = this;

    }
    #endregion

    #region Private Methods
    private void ObserveHealthKitSamples ()
    {
      // Get the starting date of the required samples
      var datePredicate = HKQuery.GetPredicateForSamples (WorkoutSession.StartDate, null, HKQueryOptions.StrictStartDate);

      // Get data from the local device
      var devices = new NSSet<HKDevice> (new HKDevice [] { HKDevice.LocalDevice });
      var devicePredicate = HKQuery.GetPredicateForObjectsFromDevices (devices);

      // Assemble compound predicate
      var queryPredicate = NSCompoundPredicate.CreateAndPredicate (new NSPredicate [] { datePredicate, devicePredicate });

      // Get ActiveEnergyBurned
      QueryActiveEnergyBurned = new HKAnchoredObjectQuery (HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned), queryPredicate, null, HKSampleQuery.NoLimit, (query, addedObjects, deletedObjects, newAnchor, error) => {
        // Valid?
        if (error == null) {
          // Yes, process all returned samples
          foreach (HKSample sample in addedObjects) {
            // Accumulate totals
            var quantitySample = sample as HKQuantitySample;
            ActiveEnergyBurned += quantitySample.Quantity.GetDoubleValue (HKUnit.Joule);

            // Save samples
            WorkoutSamples.Add (sample);
          }

          // Inform caller
          RaiseHealthKitSamplesUpdated ();
        }
      });

      // Start Query
      HealthStore.ExecuteQuery (QueryActiveEnergyBurned);

    }

    private void StopObservingHealthKitSamples ()
    {
      // Stop query
      HealthStore.StopQuery (QueryActiveEnergyBurned);
    }

    private void ResumeObservingHealthkitSamples ()
    {
      // Resume current queries
      HealthStore.ExecuteQuery (QueryActiveEnergyBurned);
    }

    private void NotifyUserOfReachedMileGoal (float miles)
    {
      // Play haptic feedback
      WKInterfaceDevice.CurrentDevice.PlayHaptic (WKHapticType.Notification);

      // Raise event
      RaiseReachedMileGoal (miles);
    }

    private void SaveWorkoutSession ()
    {
      // Build required workout quantities
      var energyBurned = HKQuantity.FromQuantity (HKUnit.Joule, ActiveEnergyBurned);
      var distance = HKQuantity.FromQuantity (HKUnit.Mile, MilesRun);

      // Create any required metadata
      var metadata = new NSMutableDictionary ();
      metadata.Add (new NSString ("HKMetadataKeyIndoorWorkout"), new NSString ("NO"));

      // Create workout
      var workout = HKWorkout.Create (HKWorkoutActivityType.Running,
                                      WorkoutSession.StartDate,
                                      NSDate.Now,
                                      WorkoutEvents.ToArray (),
                                      energyBurned,
                                      distance,
                                      metadata);

      // Save to HealthKit
      HealthStore.SaveObject (workout, (successful, error) => {
        // Handle any errors
        if (error == null) {
          // Was the save successful
          if (successful) {
            // Add samples to workout
            SaveWorkoutSamples (workout);
          }
        } else {
          // Report error
          ...
        }
      });

    }

    private void SaveWorkoutSamples (HKWorkout workout)
    {
      // Add samples to saved workout
      HealthStore.AddSamples (WorkoutSamples.ToArray (), workout, (success, error) => {
        // Handle any errors
        if (error == null) {
          // Was the save successful
          if (success) {
            ...
          }
        } else {
          // Report error
          ...
        }
      });
    }
    #endregion

    #region Public Methods
    public void PauseWorkout ()
    {
      // Pause the current workout
      HealthStore.PauseWorkoutSession (WorkoutSession);
    }

    public void ResumeWorkout ()
    {
      // Pause the current workout
      HealthStore.ResumeWorkoutSession (WorkoutSession);
    }

    public void ReachedNextMile ()
    {
      // Create and save marker event
      var markerEvent = HKWorkoutEvent.Create (HKWorkoutEventType.Marker, NSDate.Now);
      WorkoutEvents.Add (markerEvent);

      // Notify user
      NotifyUserOfReachedMileGoal (++MilesRun);
    }

    public void EndOutdoorRun ()
    {
      // End the current workout session
      HealthStore.EndWorkoutSession (WorkoutSession);
    }
    #endregion

    #region Override Methods
    public override void DidFail (HKWorkoutSession workoutSession, NSError error)
    {
      // Handle workout session failing
      RaiseFailed ();
    }

    public override void DidChangeToState (HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date)
    {
      // Take action based on the change in state
      switch (toState) {
      case HKWorkoutSessionState.NotStarted:
        break;
      case HKWorkoutSessionState.Paused:
        StopObservingHealthKitSamples ();
        RaisePaused ();
        break;
      case HKWorkoutSessionState.Running:
        if (fromState == HKWorkoutSessionState.Paused) {
          ResumeObservingHealthkitSamples ();
        } else {
          ObserveHealthKitSamples ();
        }
        RaiseRunning ();
        break;
      case HKWorkoutSessionState.Ended:
        StopObservingHealthKitSamples ();
        SaveWorkoutSession ();
        RaiseEnded ();
        break;
      }

    }

    public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
    {
      base.DidGenerateEvent (workoutSession, @event);

      // Save HealthKit generated event
      WorkoutEvents.Add (@event);

      // Take action based on the type of event
      switch (@event.Type) {
      case HKWorkoutEventType.Lap:
        ...
        break;
      case HKWorkoutEventType.Marker:
        ...
        break;
      case HKWorkoutEventType.MotionPaused:
        ...
        break;
      case HKWorkoutEventType.MotionResumed:
        ...
        break;
      case HKWorkoutEventType.Pause:
        ...
        break;
      case HKWorkoutEventType.Resume:
        ...
        break;
      }
    }
    #endregion

    #region Events
    public delegate void OutdoorRunEventDelegate ();
    public delegate void OutdoorRunMileGoalDelegate (float miles);

    public event OutdoorRunEventDelegate Failed;
    internal void RaiseFailed ()
    {
      if (this.Failed != null) this.Failed ();
    }

    public event OutdoorRunEventDelegate Paused;
    internal void RaisePaused ()
    {
      if (this.Paused != null) this.Paused ();
    }

    public event OutdoorRunEventDelegate Running;
    internal void RaiseRunning ()
    {
      if (this.Running != null) this.Running ();
    }

    public event OutdoorRunEventDelegate Ended;
    internal void RaiseEnded ()
    {
      if (this.Ended != null) this.Ended ();
    }

    public event OutdoorRunMileGoalDelegate ReachedMileGoal;
    internal void RaiseReachedMileGoal (float miles)
    {
      if (this.ReachedMileGoal != null) this.ReachedMileGoal (miles);
    }

    public event OutdoorRunEventDelegate HealthKitSamplesUpdated;
    internal void RaiseHealthKitSamplesUpdated ()
    {
      if (this.HealthKitSamplesUpdated != null) this.HealthKitSamplesUpdated ();
    }
    #endregion
  }
}

ベスト プラクティス

Apple では、watchOS 3 と iOS 10 でワークアウト アプリを設計および実装する際に、次のベスト プラクティスを使用することをお勧めします。

  • iPhone と iOS 10 バージョンのアプリに接続できない場合でも、watchOS 3 ワークアウト アプリが引き続き機能していることを確認します。
  • GPS なしで距離サンプルを生成できるため、GPS が使用できない場合は HealthKit 距離を使用します。
  • ユーザーが Apple Watch または iPhone からワークアウトを開始できるようにします。
  • アプリが他のソース (他のサード パーティのアプリなど) からのワークアウトを履歴データ ビューに表示できるようにします。
  • 削除されたワークアウトが、アプリによって履歴データに表示されないようにします。

まとめ

この記事では、Apple が watchOS 3 のワークアウト アプリに対して行った機能強化と、それらを Xamarin で実装する方法について説明しました。