Aplicaciones de entrenamiento de watchOS en Xamarin

Este artículo cubre las mejoras que Apple ha introducido en las aplicaciones de entrenamiento en watchOS 3 y cómo implementarlas en Xamarin.

Novedades de watchOS 3, las aplicaciones relacionadas con el entrenamiento tienen la capacidad de ejecutarse en segundo plano en Apple Watch y obtener acceso a los datos de HealthKit. Su aplicación principal basada en iOS 10 también tiene la capacidad de iniciar la aplicación basada en watchOS 3 sin intervención del usuario.

Los siguientes temas se tratarán en detalle:

Acerca de las aplicaciones de entrenamiento

Los usuarios de aplicaciones de fitness y entrenamiento pueden ser altamente dedicados, dedicando varias horas del día hacia sus objetivos de salud y fitness. Como resultado, esperan aplicaciones con capacidad de respuesta y fáciles de usar que recopilan y muestran datos con precisión e integran sin problemas con Apple Health.

Una aplicación de fitness o entrenamiento bien diseñada ayuda a los usuarios a trazar sus actividades para alcanzar sus objetivos de fitness. Al usar Apple Watch, las aplicaciones de fitness y entrenamiento tienen acceso instantáneo a la frecuencia cardíaca, la quemadura de calorías y la detección de actividad.

Fitness and workout app example

Novedad de watchOS 3, Background Running ofrece a las aplicaciones relacionadas con el entrenamiento la capacidad de ejecutarse en segundo plano en Apple Watch y obtener acceso a los datos de HealthKit.

En este documento se presenta la característica Ejecución en segundo plano, se explica el ciclo de vida de la aplicación de entrenamiento y se muestra cómo una aplicación de entrenamiento puede contribuir a los anillos de actividad del usuario en Apple Watch.

Acerca de las sesiones de entrenamiento

El corazón de cada aplicación de entrenamiento es una sesión de entrenamiento (HKWorkoutSession) que el usuario puede iniciar y detener. La API de sesión de entrenamiento es fácil de implementar y proporciona varias ventajas para una aplicación de entrenamiento, como:

  • Detección de movimiento y quema de calorías en función del tipo de actividad.
  • Contribución automática a los anillos de actividad del usuario.
  • Mientras se encuentra en una sesión, la aplicación se mostrará automáticamente cada vez que el usuario reactiva el dispositivo (ya sea levantando la muñeca o interactuando con Apple Watch).

Acerca de la ejecución en segundo plano

Como se indicó anteriormente, con watchOS 3, se puede establecer una aplicación de entrenamiento para ejecutarse en segundo plano. El uso de background Running una aplicación de entrenamiento puede procesar datos de los sensores de Apple Watch mientras se ejecuta en segundo plano. Por ejemplo, una aplicación puede seguir supervisando la frecuencia cardíaca del usuario, aunque ya no se muestre en pantalla.

La ejecución en segundo plano también proporciona la capacidad de presentar comentarios en directo al usuario en cualquier momento durante una sesión de entrenamiento activa, como enviar una alerta háptica para informar al usuario de su progreso actual.

Además, La ejecución en segundo plano permite a la aplicación actualizar rápidamente su interfaz de usuario para que el usuario tenga los datos más recientes cuando vea rápidamente su Apple Watch.

Para mantener un alto rendimiento en Apple Watch, una aplicación de reloj que usa Background Running debe limitar la cantidad de trabajo en segundo plano para conservar la batería. Si una aplicación usa una CPU excesiva en segundo plano, puede suspenderla watchOS.

Habilitación de la ejecución en segundo plano

Para habilitar Background Running, haga lo siguiente:

  1. En el Explorador de soluciones, haga doble clic en el archivo de Info.plist la aplicación iPhone complementario de la extensión Watch para abrirlo para su edición.

  2. Cambie a la vista Código fuente:

    The Source view

  3. Agregue una nueva clave denominada WKBackgroundModes y establezca el tipo en Array:

    Add a new key called WKBackgroundModes

  4. Agregue un nuevo elemento a la matriz con el tipo de String y un valor de workout-processing:

    Add a new item to the array with the Type of String and a value of workout-processing

  5. Guarde los cambios en el archivo.

Iniciar una sesión de entrenamiento

Hay tres pasos principales para iniciar una sesión de entrenamiento:

The three main steps to starting a Workout Session

  1. La aplicación debe solicitar autorización para acceder a los datos en HealthKit.
  2. Cree un objeto de configuración de entrenamiento para el tipo de entrenamiento que se está iniciando.
  3. Cree e inicie una sesión de entrenamiento con la configuración de entrenamiento recién creada.

Solicitud de autorización

Para que una aplicación pueda acceder a los datos de HealthKit del usuario, debe solicitar y recibir autorización del usuario. Dependiendo de la naturaleza de la aplicación de entrenamiento, puede realizar los siguientes tipos de solicitudes:

  • Autorización para escribir datos:
    • Entrenamientos
  • Autorización para leer datos:
    • Energía quemada
    • Distancia
    • Frecuencia cardíaca

Para que una aplicación pueda solicitar autorización, debe configurarse para acceder a HealthKit.

Haga lo siguiente:

  1. Haga doble clic en el archivo Entitlements.plist en el Explorador de soluciones para abrirlo para su edición.

  2. Desplácese hasta la parte inferior y active Habilitar HealthKit:

    Check Enable HealthKit

  3. Guarde los cambios en el archivo.

  4. Siga las instrucciones de las secciones Id. de aplicación explícita y Perfil de aprovisionamiento y Asociación del id. de aplicación y perfil de aprovisionamiento con la aplicación de Xamarin.iOS del artículo Introducción a HealthKit para aprovisionar correctamente la aplicación.

  5. Por último, use las instrucciones del Kit de mantenimiento de programación y solicitar permiso desde las secciones Usuario del artículo Introducción a HealthKit para solicitar autorización para acceder al almacén de datos HealthKit del usuario.

Establecer la configuración de entrenamiento

Las sesiones de entrenamiento se crean mediante un objeto de configuración de entrenamiento (HKWorkoutConfiguration) que especifica el tipo de entrenamiento (como HKWorkoutActivityType.Running) y la ubicación del entrenamiento (como HKWorkoutSessionLocationType.Outdoor):

using HealthKit;
...

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

Crear un delegado de sesión de entrenamiento

Para controlar los eventos que pueden producirse durante una sesión de entrenamiento, la aplicación deberá crear una instancia de delegado de sesión de entrenamiento. Agregue una nueva clase al proyecto y básela fuera de la clase HKWorkoutSessionDelegate. Para ver el ejemplo de una ejecución al aire libre, podría tener un aspecto similar al siguiente:

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

Esta clase crea varios eventos que se generarán como el estado de la sesión de entrenamiento cambia (DidChangeToState) y si se produce un error en la sesión de entrenamiento (DidFail).

Creación de una sesión de entrenamiento

Con la configuración de entrenamiento y el delegado de sesión de entrenamiento creado anteriormente para crear una nueva sesión de entrenamiento e iniciarla en el almacén de HealthKit predeterminado del usuario:

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

Si la aplicación inicia esta sesión de entrenamiento y el usuario vuelve a su cara de reloj, se mostrará un pequeño icono verde "hombre en ejecución" encima de la cara:

A tiny green running man icon displayed above the face

Si el usuario pulsa este icono, volverá a la aplicación.

Recopilación y control de datos

Una vez configurada e iniciada una sesión de entrenamiento, la aplicación deberá recopilar datos sobre la sesión (como la frecuencia cardíaca del usuario) y controlar el estado de la sesión:

Data Collection and Control Diagram

  1. Observación de ejemplos: la aplicación tendrá que recuperar información de HealthKit que se actuará y se mostrará al usuario.
  2. Observar eventos: la aplicación tendrá que responder a eventos generados por HealthKit o desde la interfaz de usuario de la aplicación (como el usuario que pausa el entrenamiento).
  3. Escriba Estado de ejecución: la sesión se ha iniciado y se está ejecutando actualmente.
  4. Entrar estado en pausa: el usuario ha pausado la sesión de entrenamiento actual y puede reiniciarla en una fecha posterior. El usuario puede cambiar entre los estados en ejecución y en pausa varias veces en una sola sesión de entrenamiento.
  5. Finalizar sesión de entrenamiento: en cualquier momento el usuario puede finalizar la sesión de entrenamiento o puede expirar y terminar por sí mismo si era un entrenamiento medido (por ejemplo, una carrera de dos millas).

El último paso es guardar los resultados de la sesión de entrenamiento en el almacén de datos HealthKit del usuario.

Observar ejemplos de HealthKit

La aplicación deberá abrir un delimitador de consulta de objetos para cada uno de los puntos de datos de HealthKit que le interesen, como la frecuencia cardíaca o la energía activa quemada. Para cada punto de datos que se observa, es necesario crear un controlador de actualización para capturar nuevos datos a medida que se envía a la aplicación.

A partir de estos puntos de datos, la aplicación puede acumular totales (como la distancia de ejecución total) y actualizarlo según sea necesario. Además, la aplicación puede notificar a los usuarios cuando hayan alcanzado un objetivo o logro específico, como completar la siguiente milla de una carrera.

Eche un vistazo al código de ejemplo siguiente:

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

Crea un predicado para establecer la fecha de inicio que quiere obtener datos para usar el GetPredicateForSamples método. Crea un conjunto de dispositivos para extraer información de HealthKit mediante el método GetPredicateForObjectsFromDevices, en este caso solo Apple Watch local (HKDevice.LocalDevice). Los dos predicados se combinan en un predicado compuesto (NSCompoundPredicate) mediante el método CreateAndPredicate.

Se crea un nuevo HKAnchoredObjectQuery para el punto de datos deseado (en este caso HKQuantityTypeIdentifier.ActiveEnergyBurned para el punto de datos quemado de energía activa), no se impone ningún límite en la cantidad de datos devueltos (HKSampleQuery.NoLimit) y se define un controlador de actualización para controlar los datos que se devuelven a la aplicación desde HealthKit.

Se llamará al controlador de actualizaciones cada vez que se entreguen nuevos datos a la aplicación para el punto de datos especificado. Si no se devuelve ningún error, la aplicación puede leer los datos de forma segura, realizar los cálculos necesarios y actualizar su interfaz de usuario según sea necesario.

El código recorre en bucle todos los ejemplos (HKSample) devueltos en la addedObjects matriz y los convierte en un ejemplo de cantidad (HKQuantitySample). A continuación, obtiene el doble valor de la muestra como un joule (HKUnit.Joule) y lo acumula en el total de energía activa quemada para el entrenamiento y actualiza la interfaz de usuario.

Notificación de objetivos alcanzados

Como se mencionó anteriormente, cuando el usuario logra un objetivo en la aplicación de entrenamiento (como completar la primera milla de una carrera), puede enviar comentarios hápticos al usuario a través del YTaptic Engine. La aplicación también debe actualizar la interfaz de usuario en este momento, ya que es más probable que el usuario genere su muñeca para ver el evento que ha generado los comentarios.

Para reproducir los comentarios hápticos, use el código siguiente:

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

Observación de eventos

Los eventos son marcas de tiempo que la aplicación puede usar para resaltar determinados puntos durante el entrenamiento del usuario. La aplicación creará algunos eventos directamente y se guardará en el entrenamiento y HealthKit creará automáticamente algunos eventos.

Para observar los eventos creados por HealthKit, la aplicación invalidará el DidGenerateEvent método de HKWorkoutSessionDelegate:

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 ha agregado los siguientes tipos de eventos nuevos en watchOS 3:

  • HKWorkoutEventType.Lap - Son para eventos que rompen el entrenamiento en partes de la misma distancia. Por ejemplo, para marcar una vuelta alrededor de una pista mientras se ejecuta.
  • HKWorkoutEventType.Marker - Son para puntos arbitrarios de interés dentro del entrenamiento. Por ejemplo, alcanzar un punto específico en la ruta de una carrera al aire libre.

La aplicación puede crear estos nuevos tipos y almacenarlos en el entrenamiento para usarlos posteriormente en la creación de gráficos y estadísticas.

Para crear un evento de marcador, haga lo siguiente:

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

Este código crea una nueva instancia de un evento de marcador (HKWorkoutEvent) y la guarda en una colección privada de eventos (que posteriormente se escribirá en la sesión de entrenamiento) y notifica al usuario del evento a través de hápticos.

Pausar y reanudar entrenamientos

En cualquier momento de una sesión de entrenamiento, el usuario puede pausar temporalmente el entrenamiento y reanudarlo más adelante. Por ejemplo, podrían pausar una ejecución interior para tomar una llamada importante y reanudar la ejecución una vez completada la llamada.

La interfaz de usuario de la aplicación debe proporcionar una manera de pausar y reanudar el entrenamiento (llamando a HealthKit) para que Apple Watch pueda conservar el espacio de energía y datos mientras el usuario ha suspendido su actividad. Además, la aplicación debe omitir los nuevos puntos de datos que se pueden recibir cuando la sesión de entrenamiento está en un estado en pausa.

HealthKit responderá a las llamadas de pausa y reanudación mediante la generación de eventos Pause y Resume. Mientras se pausa la sesión de entrenamiento, HealthKit no enviará ningún evento o datos nuevos a la aplicación hasta que se reanude la sesión.

Use el código siguiente para pausar y reanudar una sesión de entrenamiento:

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

Los eventos Pause y Resume que se generarán desde HealthKit se pueden controlar invalidando el DidGenerateEvent método de HKWorkoutSessionDelegate:

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

Eventos de movimiento

También es nuevo en watchOS 3, son los eventos Motion Paused (HKWorkoutEventType.MotionPaused) y Motion Resumed (HKWorkoutEventType.MotionResumed). HealthKit genera automáticamente estos eventos durante un entrenamiento en ejecución cuando el usuario comienza y deja de moverse.

Cuando la aplicación recibe un evento Motion Paused, debe dejar de recopilar datos hasta que el usuario reanude el movimiento y se reciba el evento Motion Resumes. La aplicación no debe pausar la sesión de entrenamiento en respuesta a un evento Motion Paused.

Importante

Los eventos Motion Paused y Motion Resume solo se admiten para el tipo de actividad RunningWorkout (HKWorkoutActivityType.Running).

De nuevo, estos eventos se pueden controlar invalidando el DidGenerateEvent método de HKWorkoutSessionDelegate:

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

Finalizar y guardar la sesión de entrenamiento

Cuando el usuario haya completado su entrenamiento, la aplicación tendrá que finalizar la sesión de entrenamiento actual y guardarla en la base de datos HealthKit. Los entrenamientos guardados en HealthKit se mostrarán automáticamente en la lista de actividades de entrenamiento.

Novedad de iOS 10, esto incluye también la lista Lista de actividades de entrenamiento en el iPhone del usuario. Así que incluso si el Apple Watch no está cerca, el entrenamiento se presentará en el teléfono.

Los entrenamientos que incluyen muestras de energía actualizarán el anillo de movimiento del usuario en la aplicación Actividades, por lo que las aplicaciones de terceros ahora pueden contribuir a los objetivos de movimiento diario del usuario.

Los pasos siguientes son necesarios para finalizar y guardar una sesión de entrenamiento:

Ending and Saving the Workout Session Diagram

  1. En primer lugar, la aplicación tendrá que finalizar la sesión de entrenamiento.
  2. La sesión de entrenamiento se guarda en HealthKit.
  3. Agregue cualquier muestra (como la energía quemada o la distancia) a la sesión de entrenamiento guardada.

Finalizar la sesión

Para finalizar la sesión de entrenamiento, llame al EndWorkoutSession método del HKHealthStore paso en HKWorkoutSession:

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

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

Esto restablecerá los sensores de los dispositivos a su modo normal. Cuando HealthKit termine de finalizar el entrenamiento, recibirá una devolución de llamada al DidChangeToState método de HKWorkoutSessionDelegate:

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

}

Guardar la sesión

Una vez que la aplicación haya finalizado la sesión de entrenamiento, deberá crear un entrenamiento () y guardarlo (HKWorkoutjunto con un evento) en el almacén de datos de 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
    }
  });

}

Este código crea la cantidad total de energía quemada y distancia necesaria para el entrenamiento como HKQuantity objetos. Se crea un diccionario de metadatos que definen el entrenamiento y se especifica la ubicación del entrenamiento:

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

Se crea un nuevo HKWorkout objeto con el mismo HKWorkoutActivityType que HKWorkoutSession,las fechas de inicio y finalización, la lista de eventos (que se acumulan en las secciones anteriores), la energía quemada, la distancia total y el diccionario de metadatos. Este objeto se guarda en el Almacén de estado y se controlan los errores.

Agregar ejemplos

Cuando la aplicación guarda un conjunto de muestras en un entrenamiento, HealthKit genera una conexión entre los ejemplos y el propio entrenamiento para que la aplicación pueda consultar HealthKit en una fecha posterior para todas las muestras asociadas a un entrenamiento determinado. Con esta información, la aplicación puede generar gráficos a partir de los datos de entrenamiento y trazarlos con una escala de tiempo de entrenamiento.

Para que una aplicación contribuya al anillo de movimiento de la aplicación de actividad, debe incluir muestras de energía con el entrenamiento guardado. Además, los totales de distancia y energía deben coincidir con la suma de las muestras que la aplicación asocia con un entrenamiento guardado.

Para agregar muestras a un entrenamiento guardado, haga lo siguiente:

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

Opcionalmente, la aplicación puede calcular y crear un subconjunto más pequeño de muestras o una mega muestra (que abarca todo el rango del entrenamiento) que luego se asocia con el entrenamiento guardado.

Entrenamientos e iOS 10

Cada aplicación de entrenamiento watchOS 3 tiene una aplicación de entrenamiento basada en iOS 10 principal y, nueva en iOS 10, esta aplicación de iOS se puede usar para iniciar un entrenamiento que colocará Apple Watch en el modo de entrenamiento (sin intervención del usuario) y ejecutar la aplicación watchOS en el modo de ejecución en segundo plano (vea Acerca de la ejecución en segundo plano arriba para obtener más detalles).

Mientras se ejecuta la aplicación watchOS, puede usar WatchConnectivity para la mensajería y la comunicación con la aplicación principal de iOS.

Eche un vistazo a cómo funciona este proceso:

iPhone and Apple Watch communication diagram

  1. La aplicación de iPhone crea un HKWorkoutConfiguration objeto y establece el tipo de entrenamiento y la ubicación.
  2. El HKWorkoutConfiguration objeto se envía a la versión de Apple Watch de la aplicación y, si aún no se está ejecutando, el sistema lo inicia.
  3. Con la configuración de entrenamiento pasada, la aplicación watchOS 3 inicia una nueva sesión de entrenamiento (HKWorkoutSession).

Importante

Para que la aplicación principal de iPhone inicie un entrenamiento en Apple Watch, la aplicación watchOS 3 debe tener habilitada la ejecución en segundo plano. Consulte Habilitación de la ejecución en segundo plano anterior para obtener más información.

Este proceso es muy similar al proceso de iniciar una sesión de entrenamiento en la aplicación watchOS 3 directamente. En el iPhone, use el código siguiente:

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

Este código garantiza que la versión watchOS de la aplicación esté instalada y que la versión de iPhone pueda conectarse primero a ella:

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

A continuación, crea un HKWorkoutConfiguration como de costumbre y usa el StartWatchApp método de HKHealthStore para enviarlo a Apple Watch e iniciar la aplicación y la sesión de entrenamiento.

Y en la aplicación watch OS, use el código siguiente en 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);
}

Toma HKWorkoutConfiguration y crea una nueva HKWorkoutSession instancia de y adjunta una instancia de custom HKWorkoutSessionDelegate. La sesión de entrenamiento se inicia en el HealthKit Health Store del usuario.

Reunir todas las piezas

Tomando toda la información presentada en este documento, una aplicación de entrenamiento basada en watchOS 3 y su aplicación de entrenamiento basada en iOS 10 principal podría incluir las siguientes partes:

  1. iOS 10 ViewController.cs : controla el inicio de una sesión de conectividad de inspección y un entrenamiento en Apple Watch.
  2. watchOS 3 ExtensionDelegate.cs : controla la versión watchOS 3 de la aplicación de entrenamiento.
  3. watchOS 3 OutdoorRunDelegate.cs : un personalizado HKWorkoutSessionDelegate para controlar eventos para el entrenamiento.

Importante

El código que se muestra en las secciones siguientes solo incluye las partes necesarias para implementar las nuevas características mejoradas proporcionadas a las aplicaciones de entrenamiento en watchOS 3. No se incluye todo el código auxiliar y el código para presentar y actualizar la interfaz de usuario, pero se puede crear fácilmente siguiendo nuestra otra documentación de watchOS.

ViewController.cs

El ViewController.cs archivo de la versión principal de iOS 10 de la aplicación de entrenamiento incluiría el código siguiente:

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

El ExtensionDelegate.cs archivo de la versión watchOS 3 de la aplicación de entrenamiento incluiría el código siguiente:

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

El OutdoorRunDelegate.cs archivo de la versión watchOS 3 de la aplicación de entrenamiento incluiría el código siguiente:

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

Procedimientos recomendados

Apple sugiere usar los siguientes procedimientos recomendados al diseñar e implementar aplicaciones de entrenamiento en watchOS 3 e iOS 10:

  • Asegúrese de que la aplicación watchOS 3 Entrenamiento sigue funcionando incluso cuando no se puede conectar al iPhone y a la versión de iOS 10 de la aplicación.
  • Usa la distancia de HealthKit cuando el GPS no está disponible, ya que es capaz de generar muestras de distancia sin GPS.
  • Permitir al usuario iniciar el entrenamiento desde Apple Watch o el iPhone.
  • Permitir que la aplicación muestre entrenamientos de otros orígenes (como otras aplicaciones de terceros) en sus vistas de datos históricas.
  • Asegúrese de que la aplicación no muestre los entrenamientos eliminados en los datos históricos.

Resumen

Este artículo ha cubierto las mejoras que Apple ha introducido en las aplicaciones de entrenamiento en watchOS 3 y cómo implementarlas en Xamarin.