共用方式為


Xamarin 中的 watchOS 訓練應用程式

本文涵蓋 Apple 在 watchOS 3 中鍛煉應用程式的增強功能,以及如何在 Xamarin 中實作它們。

觀看OS 3的新功能,鍛煉相關應用程式能夠在Apple Watch的背景中執行,並取得HealthKit資料的存取權。 其父 iOS 10 型應用程式也能夠在沒有使用者介入的情況下啟動 watchOS 3 型應用程式。

將會詳細介紹下列主題:

關於訓練應用程式

健身和鍛煉應用程式的使用者可以高度投入,每天數小時致力於他們的健康和健身目標。 因此,他們預期回應式、易於使用的應用程式能準確地收集和顯示數據,並與 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 的隨附 i 電話 應用程式Info.plist檔案以開啟它以進行編輯。

  2. 切換至 [來源] 檢視:

    [來源] 檢視

  3. 新增名為 WKBackgroundModes 的新索引鍵,並將 Type 設定為 Array

    新增名為 WKBackgroundModes 的新金鑰

  4. 將新專案新增至陣列,其 TypeString 為 ,且 值為 workout-processing

    將新專案新增至具有字串類型及訓練處理值的陣列

  5. 儲存對檔案所做的變更。

開始鍛煉課程

啟動鍛煉課程有三個主要步驟:

開始鍛煉課程的三個主要步驟

  1. 應用程式必須要求授權才能存取 HealthKit 中的數據。
  2. 為要啟動的鍛煉類型建立「訓練組態」物件。
  3. 使用新建立的「鍛煉設定」建立並啟動「鍛煉會話」。

要求授權

應用程式必須先向使用者要求並接收授權,應用程式才能存取使用者的 HealthKit 數據。 視鍛煉應用程式的性質而定,它可能會提出下列類型的要求:

  • 寫入資料的授權:
    • 訓練
  • 讀取資料的授權:
    • 能源燃燒
    • 距離
    • 心率

應用程式必須先設定為存取 HealthKit,才能要求授權。

執行下列操作:

  1. 在方案總管中按兩下 Entitlements.plist 檔案將其開啟以進行編輯。

  2. 捲動至底部,並檢查 [啟用 HealthKit]:

    檢查啟用 HealthKit

  3. 儲存對檔案所做的變更。

  4. 請遵循 HealthKit 簡介一文中明確應用程式識別碼和布建配置檔中的指示,以及將應用程式識別碼和布建配置檔與您的 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
  }
}

這個類別會建立數個事件,當鍛煉會話的狀態變更時,以及如果鍛煉會話失敗(DidChangeToStateDidFail)。

建立鍛煉課程

使用上面建立的 「鍛煉設定和鍛煉會話委派」來建立新的「鍛煉會話」,並針對用戶的預設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. 輸入暫停狀態 - 使用者已暫停目前的鍛煉會話,並可在稍後重新啟動。 用戶可以在單一鍛煉會話中,在執行中和暫停狀態之間切換數次。
  5. 結束鍛煉會話 - 在任何時間點,使用者可以結束鍛煉會話,或者如果它是計量訓練(例如兩英里跑),它可能會自行到期和結束。

最後一個步驟是將訓練會話的結果儲存至使用者的 HealthKit 資料存放區。

觀察 HealthKit 範例

應用程式必須針對它感興趣的每個 HealthKit 數據點開啟 錨點對象查詢 ,例如心率或作用中能量燃燒。 針對觀察到的每個數據點,必須建立更新處理程式,以在傳送至應用程式時擷取新的數據。

從這些數據點,應用程式可以累積總計(例如總運行距離),並視需要更新其使用者介面。 此外,應用程式可以在用戶達到特定目標或成就時通知使用者,例如完成下一英里的執行。

請檢視下列範例程式代碼:

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)。 這兩個述詞會使用 CreateAndPredicate 方法合併成複合述詞 (NSCompoundPredicate)。

針對所需的數據點建立新的 HKAnchoredObjectQuery (在此案例 HKQuantityTypeIdentifier.ActiveEnergyBurned 中為 Active Energy Burned 數據點),不會對傳回的數據量施加任何限制,HKSampleQuery.NoLimit而且已定義更新處理程式來處理從 HealthKit 傳回至應用程式的數據。

每當針對指定的數據點將新數據傳遞至應用程式時,都會呼叫更新處理程式。 如果未傳回任何錯誤,應用程式就可以安全地讀取數據、進行任何必要的計算,並視需要更新其 UI。

程式代碼會迴圈處理陣列中addedObjects傳回的所有樣本 (HKSample),並將其轉換成數量樣本 (HKQuantitySample)。 然後,它會取得樣本的雙精度浮點數,HKUnit.Joule並將它累積到用於鍛煉並更新使用者介面的現能總和。

已達成目標通知

如上所述,當使用者在鍛煉應用程式中達成目標時(例如完成第一英里的跑步),它就可以透過 Taptic 引擎傳送觸覺回饋給使用者。 應用程式此時也應該更新其UI,因為使用者可能會提高手腕,以查看提示意見反應的事件。

若要播放觸覺回饋,請使用下列程序代碼:

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

觀察事件

事件是應用程式可用來在用戶鍛煉期間醒目提示特定點的時間戳。 某些事件會由應用程式直接建立,並儲存到鍛煉中,而 HealthKit 會自動建立某些事件。

若要觀察 HealthKit 所建立的事件,應用程式會覆寫 DidGenerateEventHKWorkoutSessionDelegate方法:

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 - 適用於將鍛煉分成相等距離部分的事件。 例如,用於在執行時標記軌道周圍的一圈。
  • HKWorkoutEventType.Marker - 適用於鍛煉內的任意景點。 例如,到達戶外執行路線上的特定點。

這些新類型可由應用程式建立,並儲存在鍛煉中,以供稍後用於建立圖形和統計數據。

若要建立 Marker 事件,請執行下列動作:

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並將它儲存到事件的私人集合中(稍後會寫入訓練會話),並透過觸覺通知事件的使用者。

暫停和繼續鍛煉

在鍛煉會話的任何時間點,使用者可以暫時暫停鍛煉,並在稍後繼續鍛煉。 例如,他們可能會暫停室內執行以取得重要呼叫,並在呼叫完成之後繼續執行。

應用程式的UI應該提供一種方式來暫停和繼續鍛煉(藉由呼叫HealthKit),讓Apple Watch可以在使用者暫停活動時同時節省電力和數據空間。 此外,應用程式應該忽略任何在鍛煉會話處於暫停狀態時可能收到的新數據點。

HealthKit 會藉由產生Pause和Resume事件來回應暫停和繼續呼叫。 當訓練會話暫停時,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);
}

藉由覆 DidGenerateEvent 寫 的 HKWorkoutSessionDelegate方法,即可處理從 HealthKit 產生的 Pause 和 Resume 事件:

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;
  }
}

動作事件

觀看OS 3的新功能是動作暫停 (HKWorkoutEventType.MotionPaused) 和動作繼續 (HKWorkoutEventType.MotionResumed) 事件。 當用戶開始和停止移動時,HealthKit 會在跑步鍛煉期間自動引發這些事件。

當應用程式收到MotionPaused事件時,它應該停止收集數據,直到使用者繼續動作並收到Motion Resumes事件為止。 應用程式不應該暫停鍛煉會話,以回應動作暫停事件。

重要

RunningWorkout 活動類型 (HKWorkoutActivityType.Running) 僅支援 [動作暫停] 和 [動作繼續] 事件。

同樣地,您可以覆寫 的 HKWorkoutSessionDelegate方法來處理DidGenerateEvent這些事件:

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 的新功能,這也包括使用者 i 電話 上的 「鍛煉活動清單」清單。 因此,即使蘋果手錶不在附近,鍛煉將在手機上顯示。

包含能源樣本的鍛煉將會在活動應用程式中更新用戶的移動環,讓第三方應用程式現在可以為使用者的每日移動目標做出貢獻。

若要結束並儲存鍛煉會話,需要下列步驟:

結束和儲存鍛煉會話圖表

  1. 首先,應用程式必須結束訓練課程。
  2. 鍛煉會話會儲存至HealthKit。
  3. 將任何樣本(例如能量燃燒或距離)新增至已儲存的鍛煉課程。

結束會話

若要結束訓練會話,請呼叫 EndWorkoutSession 傳入 HKWorkoutSessionHKHealthStore 方法:

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

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

這會將裝置感測器重設為其一般模式。 當 HealthKit 完成訓練結束時,它會收到 的 方法HKWorkoutSessionDelegate回呼DidChangeToState

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 物件,其HKWorkoutSessionHKWorkoutActivityType 、開始和結束日期、事件清單(從上述各節累積)、耗能、總距離和元數據字典相同。 此物件會儲存至健全狀況存放區,並處理任何錯誤。

新增範例

當應用程式將一組樣本儲存至「鍛煉」時,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
    }
  });
}

或者,應用程式可以計算並建立較小的樣本子集或一個大型樣本(跨越整個範圍的鍛煉),然後與儲存的鍛煉相關聯。

鍛煉和 iOS 10

每個 watchOS 3 鍛煉應用程式都有父 iOS 10 型鍛煉應用程式,而且 iOS 10 的新功能,此 iOS 應用程式可用來啟動訓練,將 Apple Watch 放在鍛煉模式(無需使用者介入)中執行 watchOS 應用程式,並在背景執行模式中執行 watchOS 應用程式(如需詳細資訊,請參閱 關於上述背景執行 )。

當 watchOS 應用程式執行時,可以使用 Watch 連線 ivity 與父 iOS 應用程式進行傳訊和通訊。

看看此程式的運作方式:

i 電話 和 Apple Watch 通訊圖表

  1. i 電話 應用程式會HKWorkoutConfiguration建立物件,並設定「鍛煉類型和位置」。
  2. 物件 HKWorkoutConfiguration 會傳送應用程式的 Apple Watch 版本,如果尚未執行,則會由系統啟動。
  3. 使用傳入的訓練組態,watchOS 3 應用程式會啟動新的鍛煉課程 (HKWorkoutSession)。

重要

為了讓父 i 電話 應用程式在 Apple Watch 上開始鍛煉,watchOS 3 應用程式必須啟用背景執行。 如需詳細資訊,請參閱 啟用上述背景執行

此程式與直接在 watchOS 3 應用程式中啟動鍛煉會話的程式非常類似。 在 i 電話 上,使用下列程式代碼:

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 版本,且 i 電話 版本可以先連線到它:

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

然後它會像往常一樣建立 HKWorkoutConfiguration ,並使用 StartWatchAppHKHealthStore 方法將它傳送至 Apple Watch,並啟動應用程式和鍛煉會話。

在監看作業系統應用程式上,請在 中使用 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 Health Store 啟動。

將所有片段整合在一起

取得本文件中顯示的所有資訊,watchOS 3 型鍛煉應用程式及其父 iOS 10 型鍛煉應用程式可能包含下列部分:

  1. iOS 10 ViewController.cs - 處理 Watch 連線 ivity 會話的開始,以及 Apple Watch 上的鍛煉。
  2. watchOS 3 ExtensionDelegate.cs - 處理 watchOS 3 版的鍛煉應用程式。
  3. watchOS 3 OutdoorRunDelegate.cs - 用來處理鍛煉事件的自定義 HKWorkoutSessionDelegate 專案。

重要

下列各節中顯示的程式代碼只包含實作 watchOS 3 中訓練應用程式所提供之新增強功能所需的元件。 所有支援的程式代碼和要呈現和更新UI的程式代碼都不會包含,但可以遵循我們的其他 watchOS 檔輕鬆建立。

ViewController.cs

ViewController.cs上層 iOS 10 版鍛煉應用程式中的檔案會包含下列程式代碼:

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

ExtensionDelegate.cs watchOS 3 版訓練應用程式的檔案會包含下列程式代碼:

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

OutdoorRunDelegate.cs watchOS 3 版訓練應用程式的檔案會包含下列程式代碼:

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 中設計和實作訓練應用程式時,使用下列最佳做法:

  • 請確定 watchOS 3 訓練應用程式即使在無法連線到 i 電話 和 iOS 10 版本的應用程式時仍可正常運作。
  • 當 GPS 無法使用時,請使用 HealthKit 距離,因為它能夠產生沒有 GPS 的距離樣本。
  • 允許使用者從 Apple Watch 或 i 電話 開始鍛煉。
  • 允許應用程式在其歷程記錄數據檢視中顯示來自其他來源的鍛煉(例如其他第三方應用程式)。
  • 請確定應用程式不會在歷程記錄數據中顯示已刪除的訓練。

摘要

本文涵蓋 Apple 在 watchOS 3 中鍛煉應用程式的增強功能,以及如何在 Xamarin 中實作它們。