Xamarin 中的 watchOS 锻炼应用

本文介绍 Apple 对 watchOS 3 中的锻炼应用所做的增强以及如何在 Xamarin 中实现这些增强

作为 watchOS 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. 在“解决方案资源管理器”中,双击手表扩展的配套 iPhone 应用的 文件,以将其打开进行编辑Info.plist

  2. 切换到“源”视图

    源视图

  3. 添加名为 WKBackgroundModes 的新键并将“类型”设置为 Array

    添加名为 WKBackgroundModes 的新密钥

  4. 将“类型”为 String、值为 workout-processing 的新项添加到数组中:

    将新项添加到数组中,类型为字符串,值为 workout-processing。

  5. 保存对文件所做的更改。

启动锻炼会话

启动锻炼会话需要执行三个主要步骤:

启动锻炼会话需要执行三个主要步骤

  1. 应用必须请求授权才能访问 HealthKit 中的数据。
  2. 为正在启动的锻炼类型创建锻炼配置对象。
  3. 使用新建的锻炼配置创建并启动锻炼会话。

请求授权

在应用可以访问用户的 HealthKit 数据之前,它必须请求并接收用户的授权。 根据锻炼应用的性质,它可能会发出以下类型的请求:

  • 写入数据的授权:
    • 锻炼
  • 读取数据的授权:
    • 能量消耗
    • 距离
    • 心率

在应用请求授权之前,需要将它配置为访问 HealthKit。

请执行以下操作:

  1. 在“解决方案资源管理器”中,双击 Entitlements.plist 文件,将其打开进行编辑。

  2. 滚动到底部并选中“启用 HealthKit”

    检查“启用 HealthKit”

  3. 保存对文件所做的更改。

  4. 按照 HealthKit 简介一文的显式应用 ID 和预配配置文件以及将应用 ID 和预配配置文件与 Xamarin.iOS 应用相关联部分中的说明正确预配应用。

  5. 最后,按照 HealthKit 简介一文的 Health Kit 编程以及从用户请求权限部分中的说明来请求授权,以便能够访问用户的 HealthKit 数据存储。

设置锻炼配置

锻炼会话是使用指定锻炼类型(例如 HKWorkoutActivityType.Running)和锻炼位置(例如 HKWorkoutSessionLocationType.Outdoor)的锻炼配置对象 (HKWorkoutConfiguration) 创建的:

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. 进入暂停状态 - 用户已暂停当前锻炼会话,以后可以重启该会话。 用户可以在单个锻炼会话中多次在运行和暂停状态之间切换。
  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),对返回的数据量 (HKSampleQuery.NoLimit) 没有限制;并定义一个更新处理程序来处理从 HealthKit 返回到应用的数据。

每次将给定数据点的新数据传递给应用时,都会调用该更新处理程序。 如果未返回错误,则应用可以安全地读取数据、进行任何所需的计算并根据需要更新其 UI。

该代码循环访问 addedObjects 数组中返回的所有样本 (HKSample),并将其强制转换为数量样本 (HKQuantitySample)。 然后,它获取样本的双精度值(焦耳 (HKUnit.Joule)),将其累积成跑步锻炼期间消耗的活跃能量总数,并更新用户界面。

实现目标通知

如上所述,当用户在锻炼应用中实现目标(例如完成第一英里的跑步)时,它可以通过 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 - 适用于将锻炼分为等距离部分的活动。 例如,用于在跑步时绕跑道标记一圈。
  • HKWorkoutEventType.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 将通过生成暂停和恢复事件来响应暂停和恢复调用。 当锻炼会话暂停时,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);
}

可以通过重写 HKWorkoutSessionDelegateDidGenerateEvent 方法来处理从 HealthKit 生成的暂停和恢复事件:

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. 首先,应用需要结束锻炼会话。
  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
    }
  });
}

(可选)应用可以计算并创建较小的样本子集或一个大型样本(涵盖整个锻炼范围),然后将其与保存的锻炼相关联。

锻炼和 iOS 10

每个 watchOS 3 锻炼应用都有一个基于 iOS 10 的父锻炼应用,对于 iOS 10,此 iOS 应用可用于启动锻炼,将 Apple Watch 置于锻炼模式(无需用户干预)并以后台运行模式运行 watchOS 应用(有关更多详细信息,请参阅上面的关于后台运行)。

当 watchOS 应用正在运行时,它可以使用 WatchConnectivity 与父 iOS 应用进行消息传递和通信。

了解此过程的工作原理:

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,并启动应用和锻炼会话。

在 watchOS 应用上,在 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 不可用时使用 HealthKit 距离,因为它能够在没有 GPS 的情况下生成距离样本。
  • 允许用户从 Apple Watch 或 iPhone 启动锻炼应用。
  • 允许应用在其历史数据视图中显示来自其他源(例如其他第三方应用)的锻炼数据。
  • 确保应用不会在历史数据中显示已删除的锻炼。

总结

本文介绍了 Apple 对 watchOS 3 中的锻炼应用所做的增强以及如何在 Xamarin 中实现这些增强。