Partager via


watchOS Workout Apps in Xamarin

Cet article décrit les améliorations apportées par Apple aux applications d’entraînement dans watchOS 3 et comment les implémenter dans Xamarin.

Nouveautés de watchOS 3, les applications associées à l’entraînement ont la possibilité de s’exécuter en arrière-plan sur Apple Watch et d’accéder aux données HealthKit. Leur application iOS 10 parente a également la possibilité de lancer l’application watchOS 3 sans intervention de l’utilisateur.

Les rubriques suivantes sont traitées en détail :

À propos des applications d’entraînement

Les utilisateurs d’applications de fitness et d’entraînement peuvent être hautement dédiés, en dévotant plusieurs heures de la journée à leurs objectifs de santé et de fitness. Par conséquent, ils s’attendent à des applications réactives et faciles à utiliser qui collectent et affichent avec précision les données et s’intègrent en toute transparence à Apple Health.

Une application de fitness ou d’entraînement bien conçue aide les utilisateurs à tracer leurs activités pour atteindre leurs objectifs de mise en forme. En utilisant les applications Apple Watch, de fitness et d’entraînement ont un accès instantané à la fréquence cardiaque, à la brûlure de calories et à la détection d’activité.

Exemple d’application de fitness et d’entraînement

Nouveautés de watchOS 3, Background Running offre aux applications associées à l’entraînement la possibilité de s’exécuter en arrière-plan sur Apple Watch et d’accéder aux données HealthKit.

Ce document présente la fonctionnalité Background Running, couvre le cycle de vie de l’application d’entraînement et montre comment une application d’entraînement peut contribuer aux anneaux d’activité de l’utilisateur sur l’Apple Watch.

À propos des sessions d’entraînement

Le cœur de chaque application d’entraînement est une session d’entraînement (HKWorkoutSession) que l’utilisateur peut démarrer et arrêter. L’API Session d’entraînement est facile à implémenter et offre plusieurs avantages à une application d’entraînement, par exemple :

  • Détection des brûlures de mouvement et de calories en fonction du type d’activité.
  • Contribution automatique aux anneaux d’activité de l’utilisateur.
  • Dans une session, l’application s’affiche automatiquement chaque fois que l’utilisateur réveille l’appareil (soit en soulevant son poignet, soit en interagissant avec l’Apple Watch).

À propos de l’exécution en arrière-plan

Comme indiqué ci-dessus, avec watchOS 3, une application d’entraînement peut être définie pour s’exécuter en arrière-plan. L’utilisation de l’arrière-plan d’une application d’entraînement peut traiter les données des capteurs d’Apple Watch lors de l’exécution en arrière-plan. Par exemple, une application peut continuer à surveiller la fréquence cardiaque de l’utilisateur, même si elle n’est plus affichée à l’écran.

L’exécution en arrière-plan permet également de présenter des commentaires en direct à l’utilisateur à tout moment pendant une session d’entraînement active, par exemple l’envoi d’une alerte haptique pour informer l’utilisateur de sa progression actuelle.

En outre, l’exécution en arrière-plan permet à l’application de mettre à jour rapidement son interface utilisateur afin que l’utilisateur dispose des données les plus récentes lorsqu’il regarde rapidement son Apple Watch.

Pour maintenir des performances élevées sur Apple Watch, une application espion à l’aide de l’exécution en arrière-plan doit limiter la quantité de travail en arrière-plan pour économiser la batterie. Si une application utilise un processeur excessif en arrière-plan, elle peut être suspendue par watchOS.

Activation de l’exécution en arrière-plan

Pour activer l’exécution en arrière-plan, procédez comme suit :

  1. Dans le Explorateur de solutions, double-cliquez sur le fichier de l’application i Téléphone complémentaire de l’extension Info.plist Watch pour l’ouvrir pour modification.

  2. Basculez vers la vue Source :

    Vue Source

  3. Ajoutez une nouvelle clé appelée WKBackgroundModes et définissez le type sur Array:

    Ajouter une nouvelle clé appelée W Ko ackgroundModes

  4. Ajoutez un nouvel élément au tableau avec le type et String une valeur de workout-processing:

    Ajouter un nouvel élément au tableau avec le type de chaîne et une valeur de traitement de l’entraînement

  5. Enregistrez les modifications du fichier.

Démarrage d’une session d’entraînement

Il existe trois étapes principales pour commencer une session d’entraînement :

Les trois principales étapes de démarrage d’une session d’entraînement

  1. L’application doit demander l’autorisation d’accéder aux données dans HealthKit.
  2. Créez un objet De configuration d’entraînement pour le type d’entraînement démarré.
  3. Créez et démarrez une session d’entraînement à l’aide de la configuration d’entraînement nouvellement créée.

Demande d’autorisation

Pour qu’une application puisse accéder aux données HealthKit de l’utilisateur, elle doit demander et recevoir l’autorisation de l’utilisateur. En fonction de la nature de l’application d’entraînement, il peut effectuer les types de demandes suivants :

  • Autorisation d’écrire des données :
    • Entraînements
  • Autorisation de lecture des données :
    • Énergie brûlée
    • Distance
    • Fréquence cardiaque

Pour qu’une application puisse demander une autorisation, elle doit être configurée pour accéder à HealthKit.

Effectuez les actions suivantes :

  1. Dans l’Explorateur de solutions, double-cliquez sur le fichier Entitlements.plist pour l’ouvrir et le modifier.

  2. Faites défiler jusqu’au bas et case activée Activer HealthKit :

    Vérifier activer HealthKit

  3. Enregistrez les modifications du fichier.

  4. Suivez les instructions de l’ID d’application explicite et du profil d’approvisionnement et associez l’ID d’application et le profil d’approvisionnement à vos sections d’application Xamarin.iOS de l’article Introduction to HealthKit pour provisionner correctement l’application.

  5. Enfin, utilisez les instructions du Kit d’intégrité de programmation et demande l’autorisation de l’utilisateur dans l’article Introduction à HealthKit pour demander l’autorisation d’accéder au magasin de données HealthKit de l’utilisateur.

Définition de la configuration de l’entraînement

Les sessions d’entraînement sont créées à l’aide d’un objet De configuration d’entraînement (HKWorkoutConfiguration) qui spécifie le type d’entraînement (par exemple HKWorkoutActivityType.Running) et l’emplacement d’entraînement (par HKWorkoutSessionLocationType.Outdoorexemple) :

using HealthKit;
...

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

Création d’un délégué de session d’entraînement

Pour gérer les événements qui peuvent se produire pendant une session d’entraînement, l’application doit créer une instance de délégué de session d’entraînement. Ajoutez une nouvelle classe au projet et basez-la hors de la HKWorkoutSessionDelegate classe. Pour l’exemple d’une course extérieure, il peut ressembler à ce qui suit :

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

Cette classe crée plusieurs événements qui seront déclenchés en tant qu’état de la session d’entraînement (DidChangeToState) et si la session d’entraînement échoue (DidFail).

Création d’une session d’entraînement

À l’aide de la configuration d’entraînement et du délégué de session d’entraînement créés ci-dessus pour créer une session d’entraînement et la démarrer sur le magasin HealthKit par défaut de l’utilisateur :

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 l’application démarre cette session d’entraînement et que l’utilisateur revient à son visage de montre, une icône « homme en cours d’exécution » verte s’affiche au-dessus du visage :

Une petite icône d’homme en cours d’exécution verte affichée au-dessus du visage

Si l’utilisateur appuie sur cette icône, il est renvoyé à l’application.

Collecte et contrôle des données

Une fois qu’une session d’entraînement a été configurée et démarrée, l’application doit collecter des données sur la session (par exemple, la fréquence cardiaque de l’utilisateur) et contrôler l’état de la session :

Diagramme de collecte et de contrôle de données

  1. Observation d’exemples : l’application doit récupérer des informations de HealthKit qui seront exécutées et affichées à l’utilisateur.
  2. Observation d’événements : l’application doit répondre aux événements générés par HealthKit ou à partir de l’interface utilisateur de l’application (par exemple, l’utilisateur interrompt l’entraînement).
  3. Entrer l’état d’exécution : la session a été démarrée et est en cours d’exécution.
  4. Entrez l’état suspendu : l’utilisateur a suspendu la session d’entraînement actuelle et peut le redémarrer à une date ultérieure. L’utilisateur peut basculer entre les états en cours d’exécution et suspendus plusieurs fois dans une session d’entraînement unique.
  5. Fin de session d’entraînement - À tout moment, l’utilisateur peut mettre fin à la session d’entraînement ou il peut expirer et se terminer par lui-même s’il s’agissait d’une séance d’entraînement limitée (par exemple, une course de deux milles).

La dernière étape consiste à enregistrer les résultats de la session d’entraînement dans le magasin de données HealthKit de l’utilisateur.

Observation d’exemples HealthKit

L’application doit ouvrir une requête d’objet Anchor pour chacun des points de données HealthKit qui lui intéressent, comme la fréquence cardiaque ou l’énergie active brûlée. Pour chaque point de données observé, un gestionnaire de mises à jour doit être créé pour capturer de nouvelles données à mesure qu’elles sont envoyées à l’application.

À partir de ces points de données, l’application peut accumuler des totaux (par exemple, la distance d’exécution totale) et mettre à jour son interface utilisateur en fonction des besoins. En outre, l’application peut avertir les utilisateurs lorsqu’ils ont atteint un objectif ou une réalisation spécifique, par exemple terminer le mile suivant d’une exécution.

Examinez l’exemple de code suivant :

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

}

Il crée un prédicat pour définir la date de début à laquelle il souhaite obtenir des données à l’aide de la GetPredicateForSamples méthode. Il crée un ensemble d’appareils pour extraire les informations HealthKit à partir de la GetPredicateForObjectsFromDevices méthode, dans ce cas, l’Apple Watch local uniquement (HKDevice.LocalDevice). Les deux prédicats sont combinés en un prédicat composé (NSCompoundPredicate) à l’aide de la CreateAndPredicate méthode.

Un nouveau HKAnchoredObjectQuery point de données est créé pour le point de données souhaité (dans ce cas HKQuantityTypeIdentifier.ActiveEnergyBurned pour le point de données Actif Energy Burned), aucune limite n’est imposée à la quantité de données retournées (HKSampleQuery.NoLimit) et un gestionnaire de mise à jour est défini pour gérer les données renvoyées à l’application à partir de HealthKit.

Le gestionnaire de mises à jour est appelé chaque fois que de nouvelles données sont remises à l’application pour le point de données donné. Si aucune erreur n’est retournée, l’application peut lire les données en toute sécurité, effectuer les calculs requis et mettre à jour son interface utilisateur selon les besoins.

Le code effectue une boucle sur tous les exemples (HKSample) retournés dans le addedObjects tableau et les convertit en un échantillon quantity (HKQuantitySample). Ensuite, il obtient la double valeur de l’échantillon en tant que milliseconde (HKUnit.Joule) et l’accumule dans le total en cours d’exécution de l’énergie active brûlée pour l’entraînement et met à jour l’interface utilisateur.

Notification d’objectif atteinte

Comme mentionné ci-dessus, lorsque l’utilisateur atteint un objectif dans l’application d’entraînement (par exemple, terminer le premier mile d’une exécution), il peut envoyer des commentaires haptiques à l’utilisateur via le moteur taptic. L’application doit également mettre à jour son interface utilisateur à ce stade, car l’utilisateur déclenchera plus que probablement son poignet pour voir l’événement qui a invité les commentaires.

Pour lire les commentaires haptiques, utilisez le code suivant :

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

Observation d’événements

Les événements sont des horodatages que l’application peut utiliser pour mettre en surbrillance certains points pendant l’entraînement de l’utilisateur. Certains événements seront créés directement par l’application et enregistrés dans l’entraînement et certains événements seront créés automatiquement par HealthKit.

Pour observer les événements créés par HealthKit, l’application remplace la DidGenerateEvent méthode du 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 a ajouté les nouveaux types d’événements suivants dans watchOS 3 :

  • HKWorkoutEventType.Lap - Sont destinés aux événements qui interrompent l’entraînement en portions de distance égales. Par exemple, pour marquer un tour autour d’une piste en cours d’exécution.
  • HKWorkoutEventType.Marker - Sont pour des points d’intérêt arbitraires au sein de l’entraînement. Par exemple, atteindre un point spécifique sur la route d’une course extérieure.

Ces nouveaux types peuvent être créés par l’application et stockés dans l’entraînement pour une utilisation ultérieure dans la création de graphiques et de statistiques.

Pour créer un événement de marqueur, procédez comme suit :

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

Ce code crée une instance d’un événement marqueur (HKWorkoutEvent) et l’enregistre dans une collection privée d’événements (qui seront écrits ultérieurement dans la session d’entraînement) et avertit l’utilisateur de l’événement via des haptics.

Suspension et reprise des séances d’entraînement

À tout moment dans une session d’entraînement, l’utilisateur peut suspendre temporairement l’entraînement et le reprendre ultérieurement. Par exemple, ils peuvent suspendre une exécution d’intérieur pour prendre un appel important et reprendre l’exécution une fois l’appel terminé.

L’interface utilisateur de l’application doit fournir un moyen de suspendre et de reprendre l’entraînement (en appelant HealthKit) afin que l’Apple Watch puisse conserver à la fois l’alimentation et l’espace de données pendant que l’utilisateur a suspendu son activité. En outre, l’application doit ignorer les nouveaux points de données qui peuvent être reçus lorsque la session d’entraînement est dans un état suspendu.

HealthKit répond aux appels de pause et de reprise en générant des événements Pause et Resume. Pendant que la session d’entraînement est suspendue, aucun nouvel événement ou données n’est envoyé à l’application par HealthKit jusqu’à ce que la session soit reprise.

Utilisez le code suivant pour suspendre et reprendre une session d’entraînement :

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

Les événements Pause et Resume qui seront générés à partir de HealthKit peuvent être gérés en remplaçant la DidGenerateEvent méthode du 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;
  }
}

Événements de mouvement

En outre, les événements Motion Paused (HKWorkoutEventType.MotionPaused) et Motion Resumed (HKWorkoutEventType.MotionResumed) sont également nouveaux pour watchOS 3. Ces événements sont déclenchés automatiquement par HealthKit lors d’une séance d’entraînement en cours d’exécution lorsque l’utilisateur démarre et cesse de se déplacer.

Lorsque l’application reçoit un événement Motion Paused, elle doit cesser de collecter des données jusqu’à ce que l’utilisateur reprend le mouvement et que l’événement Motion Resumes soit reçu. L’application ne doit pas suspendre la session d’entraînement en réponse à un événement motion suspendu.

Important

Les événements Motion Paused and Motion Resume ne sont pris en charge que pour le type d’activité RunningWorkout (HKWorkoutActivityType.Running).

Là encore, ces événements peuvent être gérés en remplaçant la DidGenerateEvent méthode du 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;
  }
}

Fin et enregistrement de la session d’entraînement

Lorsque l’utilisateur a terminé son entraînement, l’application doit mettre fin à la session d’entraînement actuelle et l’enregistrer dans la base de données HealthKit. Les séances d’entraînement enregistrées dans HealthKit sont automatiquement affichées dans la liste des activités d’entraînement.

Nouveautés d’iOS 10, cela inclut également la liste des activités d’entraînement sur l’i Téléphone de l’utilisateur. Ainsi, même si l’Apple Watch n’est pas à proximité, l’entraînement sera présenté sur le téléphone.

Les séances d’entraînement qui incluent Energy Samples mettent à jour l’anneau de déplacement de l’utilisateur dans l’application Activités afin que les applications tierces puissent désormais contribuer aux objectifs de déplacement quotidiens de l’utilisateur.

Les étapes suivantes sont nécessaires pour terminer et enregistrer une session d’entraînement :

Fin et enregistrement du diagramme de session d’entraînement

  1. Tout d’abord, l’application doit mettre fin à la session d’entraînement.
  2. La session d’entraînement est enregistrée dans HealthKit.
  3. Ajoutez des échantillons (comme l’énergie brûlée ou la distance) à la session d’entraînement enregistrée.

Fin de la session

Pour mettre fin à la session d’entraînement, appelez la EndWorkoutSession méthode du passage dans le HKHealthStoreHKWorkoutSession:

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

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

Cela réinitialise les capteurs des appareils en mode normal. Lorsque HealthKit termine l’entraînement, il reçoit un rappel à la DidChangeToState méthode du 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;
  }

}

Enregistrement de la session

Une fois que l’application a terminé la session d’entraînement, elle devra créer une séance d’entraînement (HKWorkout) et l’enregistrer (ainsi qu’un événement) dans le magasin de données 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
    }
  });

}

Ce code crée la quantité totale d’énergie brûlée et de distance pour l’entraînement en tant qu’objets HKQuantity . Un dictionnaire de métadonnées définissant l’entraînement est créé et l’emplacement de l’entraînement est spécifié :

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

Un nouvel HKWorkout objet est créé avec le même HKWorkoutActivityType que les HKWorkoutSessiondates de début et de fin, la liste des événements (cumulés à partir des sections ci-dessus), l’énergie brûlée, la distance totale et le dictionnaire de métadonnées. Cet objet est enregistré dans le Magasin d’intégrité et toutes les erreurs gérées.

Ajout d’exemples

Lorsque l’application enregistre un ensemble d’échantillons dans une séance d’entraînement, HealthKit génère une connexion entre les échantillons et l’entraînement lui-même afin que l’application puisse interroger HealthKit à une date ultérieure pour tous les échantillons associés à une séance d’entraînement donnée. À l’aide de ces informations, l’application peut générer des graphiques à partir des données d’entraînement et les tracer par rapport à un chronologie d’entraînement.

Pour qu’une application contribue à l’anneau de déplacement de l’application Activité, elle doit inclure des échantillons d’énergie avec l’entraînement enregistré. En outre, les totaux pour la distance et l’énergie doivent correspondre à la somme des échantillons que l’application associe à un entraînement enregistré.

Pour ajouter des exemples à une séance d’entraînement enregistrée, procédez comme suit :

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

Si vous le souhaitez, l’application peut calculer et créer un sous-ensemble plus petit d’échantillons ou un méga échantillon (couvrant toute la plage de l’entraînement) qui est ensuite associé à l’entraînement enregistré.

Entraînements et iOS 10

Chaque application d’entraînement watchOS 3 a une application d’entraînement parente iOS 10 et, nouveauté d’iOS 10, cette application iOS peut être utilisée pour démarrer un entraînement qui place l’Apple Watch en mode entraînement (sans intervention de l’utilisateur) et exécute l’application watchOS en mode Exécution en arrière-plan (voir À propos de l’exécution en arrière-plan ci-dessus pour plus de détails).

Pendant que l’application watchOS est en cours d’exécution, elle peut utiliser Watch Connecter ivity pour la messagerie et la communication avec l’application iOS parente.

Examinez le fonctionnement de ce processus :

Diagramme de communication i Téléphone et Apple Watch

  1. L’application i Téléphone crée un HKWorkoutConfiguration objet et définit le type d’entraînement et l’emplacement.
  2. L’objet HKWorkoutConfiguration est envoyé la version Apple Watch de l’application et, s’il n’est pas déjà en cours d’exécution, il est démarré par le système.
  3. À l’aide de la configuration de l’entraînement passée, l’application watchOS 3 démarre une nouvelle session d’entraînement (HKWorkoutSession).

Important

Pour que l’application i Téléphone parente démarre une séance d’entraînement sur Apple Watch, l’application watchOS 3 doit avoir l’option Background Running activée. Pour plus d’informations, consultez l’activation de l’arrière-plan en cours d’exécution ci-dessus.

Ce processus est très similaire au processus de démarrage d’une session d’entraînement dans l’application watchOS 3 directement. Sur i Téléphone, utilisez le code suivant :

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

Ce code garantit que la version watchOS de l’application est installée et que la version i Téléphone peut d’abord se connecter à celle-ci :

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

Ensuite, il crée une HKWorkoutConfiguration méthode comme d’habitude et utilise la StartWatchApp méthode de l’envoyer HKHealthStore à l’Apple Watch et démarrer l’application et la session d’entraînement.

Sur l’application espion du système d’exploitation, utilisez le code suivant dans :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);
}

Elle prend et HKWorkoutConfiguration crée une HKWorkoutSession instance personnalisée et attache une instance de la commande personnalisée HKWorkoutSessionDelegate. La session d’entraînement est démarrée sur healthKit Health Store de l’utilisateur.

Rassembler toutes les pièces ensemble

Prendre toutes les informations présentées dans ce document, une application d’entraînement watchOS 3 et son application d’entraînement parente iOS 10 peut inclure les parties suivantes :

  1. iOS 10 ViewController.cs - Gère le démarrage d’une session watch Connecter ivity et une séance d’entraînement sur l’Apple Watch.
  2. watchOS 3 ExtensionDelegate.cs - Gère la version watchOS 3 de l’application d’entraînement.
  3. watchOS 3 OutdoorRunDelegate.cs - Personnalisé HKWorkoutSessionDelegate pour gérer les événements pour l’entraînement.

Important

Le code présenté dans les sections suivantes inclut uniquement les parties requises pour implémenter les nouvelles fonctionnalités améliorées fournies aux applications d’entraînement dans watchOS 3. Tout le code de prise en charge et le code à présenter et mettre à jour l’interface utilisateur ne sont pas inclus, mais peuvent être facilement créés en suivant notre autre documentation watchOS.

ViewController.cs

Le ViewController.cs fichier dans la version iOS 10 parente de l’application d’entraînement inclut le code suivant :

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

Le ExtensionDelegate.cs fichier de la version watchOS 3 de l’application d’entraînement inclut le code suivant :

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

Le OutdoorRunDelegate.cs fichier de la version watchOS 3 de l’application d’entraînement inclut le code suivant :

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

Meilleures pratiques

Apple suggère d’utiliser les meilleures pratiques suivantes lors de la conception et de l’implémentation d’applications d’entraînement dans watchOS 3 et iOS 10 :

  • Assurez-vous que l’application watchOS 3 Workout est toujours fonctionnelle même lorsqu’elle ne parvient pas à se connecter à l’i Téléphone et à la version iOS 10 de l’application.
  • Utilisez la distance HealthKit lorsque le GPS n’est pas disponible, car il est en mesure de générer des échantillons de distance sans GPS.
  • Autoriser l’utilisateur à démarrer l’entraînement à partir de l’Apple Watch ou de l’i Téléphone.
  • Autoriser l’application à afficher des séances d’entraînement à partir d’autres sources (telles que d’autres applications tierces) dans ses vues de données historiques.
  • Vérifiez que l’application n’affiche pas les séances d’entraînement supprimées dans les données historiques.

Résumé

Cet article a abordé les améliorations apportées par Apple aux applications d’entraînement dans watchOS 3 et comment les implémenter dans Xamarin.