Compartilhar via


Aplicativos de treino watchOS em Xamarin

Este artigo aborda as melhorias que a Apple fez para treinar aplicativos no watchOS 3 e como implementá-los no Xamarin.

Novidade no watchOS 3, os aplicativos relacionados a exercícios têm a capacidade de rodar em segundo plano no Apple Watch e ter acesso aos dados do HealthKit. Seu aplicativo pai baseado no iOS 10 também tem a capacidade de iniciar o aplicativo baseado no watchOS 3 sem intervenção do usuário.

Os tópicos a seguir serão abordados em detalhes:

Sobre aplicativos de treino

Os usuários de aplicativos de fitness e treino podem ser altamente dedicados, dedicando várias horas do dia para seus objetivos de saúde e fitness. Como resultado, eles esperam aplicativos responsivos e fáceis de usar que coletam e exibem dados com precisão e se integram perfeitamente ao Apple Health.

Um aplicativo de fitness ou treino bem projetado ajuda os usuários a traçar suas atividades para atingir suas metas de condicionamento físico. Ao usar o Apple Watch, os aplicativos de fitness e exercícios têm acesso instantâneo à frequência cardíaca, queima de calorias e detecção de atividade.

Exemplo de aplicativo de fitness e treino

Novo no watchOS 3, o Background Running dá aos aplicativos relacionados ao treino a capacidade de rodar em segundo plano no Apple Watch e obter acesso aos dados do HealthKit.

Este documento apresentará o recurso Corrida em segundo plano, abordará o ciclo de vida do aplicativo de treino e mostrará como um aplicativo de treino pode contribuir para os Anéis de Atividade do usuário no Apple Watch.

Sobre as sessões de treino

O coração de cada aplicativo de treino é uma Sessão de Treino (HKWorkoutSession) que o usuário pode iniciar e parar. A API Workout Session é fácil de implementar e fornece vários benefícios para um aplicativo de treino, como:

  • Detecção de queima de movimento e calorias com base no Tipo de Atividade.
  • Contribuição automática para os Anéis de Atividade do usuário.
  • Durante uma sessão, o aplicativo será exibido automaticamente sempre que o usuário acordar o dispositivo (seja levantando o pulso ou interagindo com o Apple Watch).

Sobre a execução em segundo plano

Como dito acima, com o watchOS 3 um aplicativo de treino pode ser configurado para ser executado em segundo plano. Usando o plano de fundo Executar um aplicativo de treino pode processar dados dos sensores do Apple Watch enquanto é executado em segundo plano. Por exemplo, um aplicativo pode continuar monitorando a frequência cardíaca do usuário, mesmo que ele não seja mais exibido na tela.

A Corrida em Segundo Plano também fornece a capacidade de apresentar feedback ao vivo para o usuário a qualquer momento durante uma Sessão de Treino ativa, como enviar um alerta háptico para informar o usuário sobre seu progresso atual.

Além disso, a Execução em Segundo Plano permite que o aplicativo atualize rapidamente sua Interface do Usuário para que o usuário tenha os dados mais recentes quando olhar rapidamente para o Apple Watch.

Para manter o alto desempenho no Apple Watch, um aplicativo de relógio que usa a Execução em Segundo Plano deve limitar a quantidade de trabalho em segundo plano para economizar bateria. Se um aplicativo estiver usando CPU excessiva em segundo plano, ele poderá ser suspenso pelo watchOS.

Habilitando a execução em segundo plano

Para habilitar a Execução em Segundo Plano, faça o seguinte:

  1. No Gerenciador de Soluções, clique duas vezes no arquivo do Info.plist aplicativo iPhone complementar da Extensão de Observação para abri-lo para edição.

  2. Alterne para a guia Código-fonte:

    O modo de exibição Código-fonte

  3. Adicione uma nova chave chamada WKBackgroundModes e defina o Tipo como Array:

    Adicione uma nova chave chamada WKBackgroundModes

  4. Adicione um novo item à matriz com o Tipo de String e um valor de workout-processing:

    Adicione um novo item à matriz com o Tipo de Cadeia de Caracteres e um valor de processamento de treino

  5. Salve as alterações no arquivo.

Iniciando uma sessão de treino

Existem três passos principais para iniciar uma sessão de treino:

Os três principais passos para iniciar uma sessão de treino

  1. O aplicativo deve solicitar autorização para acessar dados no HealthKit.
  2. Crie um objeto Configuração de Treino para o tipo de treino que está sendo iniciado.
  3. Crie e inicie uma Sessão de Treino usando a Configuração de Treino recém-criada.

Solicitando Autorização

Antes que um aplicativo possa acessar os dados do HealthKit do usuário, ele deve solicitar e receber autorização do usuário. Dependendo da natureza do aplicativo de treino, ele pode fazer os seguintes tipos de solicitações:

  • Autorização para gravar dados:
    • Exercícios
  • Autorização para leitura de dados:
    • Energia queimada
    • Distância
    • Frequência Cardíaca

Antes que um aplicativo possa solicitar autorização, ele precisa ser configurado para acessar o HealthKit.

Faça o seguinte:

  1. No Gerenciador de Soluções, clique duas vezes no arquivo Entitlements.plist para abri-lo para edição.

  2. Role até a parte inferior e marque Ativar HealthKit:

    Marque Ativar HealthKit

  3. Salve as alterações no arquivo.

  4. Siga as instruções nas seções ID de aplicativo explícito e perfil de provisionamento e Associando a ID do aplicativo e o perfil de provisionamento ao seu aplicativo Xamarin.iOS do artigo Introdução ao HealthKit para provisionar corretamente o aplicativo.

  5. Finalmente, use as instruções nas seções Programming Health Kit e Requesting Permission From the User do artigo Introduction to HealthKit para solicitar autorização para acessar o armazenamento de dados HealthKit do usuário.

Definindo a configuração do treino

As Sessões de Treino são criadas usando um objeto Configuração de Treino (HKWorkoutConfiguration) que especifica o tipo de treino (como HKWorkoutActivityType.Running) e o local de treino (como HKWorkoutSessionLocationType.Outdoor):

using HealthKit;
...

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

Criando um delegado de sessão de treino

Para manipular os eventos que podem ocorrer durante uma Sessão de Treino, o aplicativo precisará criar uma instância de Delegado de Sessão de Treino. Adicione uma nova classe ao projeto e baseie-a fora da HKWorkoutSessionDelegate classe. Para o exemplo de uma corrida ao ar livre, pode ser semelhante ao seguinte:

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

Essa classe cria vários eventos que serão gerados à medida que o estado da Sessão de Treino for alterado (DidChangeToState) e se a Sessão de Treino falhar (DidFail).

Criando uma sessão de treino

Usando a Configuração de Treino e o Delegado de Sessão de Treino criados acima para criar uma nova Sessão de Treino e iniciá-la na loja padrão do HealthKit do usuário:

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

Se o aplicativo iniciar esta sessão de treino e o usuário voltar para o mostrador do relógio, um pequeno ícone verde de "homem correndo" será exibido acima do rosto:

Um pequeno ícone verde de homem correndo exibido acima do rosto

Se o usuário tocar nesse ícone, ele será levado de volta ao aplicativo.

Coleta e Controle de Dados

Depois que uma Sessão de Treino for configurada e iniciada, o aplicativo precisará coletar dados sobre a sessão (como a frequência cardíaca do usuário) e controlar o estado da sessão:

Diagrama de Coleta e Controle de Dados

  1. Observando amostras - O aplicativo precisará recuperar informações do HealthKit que serão acionadas e exibidas ao usuário.
  2. Observando eventos - O aplicativo precisará responder a eventos gerados pelo HealthKit ou pela interface do usuário do aplicativo (como o usuário pausando o treino).
  3. Enter Running State - A sessão foi iniciada e está em execução no momento.
  4. Inserir estado pausado - O usuário pausou a sessão de treino atual e pode reiniciá-la posteriormente. O usuário pode alternar entre os estados de corrida e pausa várias vezes em uma única sessão de treino.
  5. Terminar Sessão de Treino - A qualquer momento, o usuário pode terminar a Sessão de Treino ou ela pode expirar e terminar por conta própria se foi um treino medido (como uma corrida de duas milhas).

A etapa final é salvar os resultados da Sessão de Treino no armazenamento de dados HealthKit do usuário.

Observando amostras do HealthKit

O aplicativo precisará abrir uma Consulta de Objeto Âncora para cada um dos pontos de dados do HealthKit nos quais está interessado, como frequência cardíaca ou energia ativa queimada. Para cada ponto de dados observado, um manipulador de atualização precisará ser criado para capturar novos dados à medida que são enviados ao aplicativo.

A partir desses pontos de dados, o aplicativo pode acumular totais (como a distância total de execução) e atualizar sua Interface do Usuário conforme necessário. Além disso, o aplicativo pode notificar os usuários quando eles atingirem uma meta ou conquista específica, como completar a próxima milha de uma corrida.

Dê uma olhada no seguinte código de exemplo:

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

}

Ele cria um predicado para definir a data de início que deseja obter dados para usar o GetPredicateForSamples método. Ele cria um conjunto de dispositivos para extrair informações do HealthKit usando o GetPredicateForObjectsFromDevices método, neste caso apenas o Apple Watch local (HKDevice.LocalDevice). Os dois predicados são combinados em um Predicado Composto (NSCompoundPredicate) usando o CreateAndPredicate método.

Um novo HKAnchoredObjectQuery é criado para o ponto de dados desejado (neste caso HKQuantityTypeIdentifier.ActiveEnergyBurned , para o ponto de dados Active Energy Burned), nenhum limite é imposto sobre a quantidade de dados retornados (HKSampleQuery.NoLimit) e um manipulador de atualização é definido para lidar com os dados que estão sendo retornados ao aplicativo a partir do HealthKit.

O manipulador de atualização será chamado sempre que novos dados forem entregues ao aplicativo para o ponto de dados especificado. Se nenhum erro for retornado, o aplicativo poderá ler os dados com segurança, fazer os cálculos necessários e atualizar sua interface do usuário conforme necessário.

O código faz um loop sobre todas as amostras (HKSample) retornadas na matriz e as converte em addedObjects uma Amostra de Quantidade (HKQuantitySample). Em seguida, ele obtém o valor duplo da amostra como um joule (HKUnit.Joule) e o acumula no total de energia ativa queimada para o treino e atualiza a Interface do Usuário.

Notificação de Meta Atingida

Como mencionado acima, quando o usuário atinge um objetivo no aplicativo de treino (como completar a primeira milha de uma corrida), ele pode enviar feedback háptico para o usuário através do Taptic Engine. O aplicativo também deve atualizar sua interface do usuário neste momento, já que o usuário provavelmente levantará o pulso para ver o evento que motivou o feedback.

Para reproduzir o feedback háptico, use o seguinte código:

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

Observando Eventos

Eventos são carimbos de data/hora que o aplicativo pode usar para destacar determinados pontos durante o treino do usuário. Alguns eventos serão criados diretamente pelo aplicativo e salvos no treino e alguns eventos serão criados automaticamente pelo HealthKit.

Para observar eventos criados pelo HealthKit, o aplicativo substituirá o DidGenerateEvent método do 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;
  }
}

A Apple adicionou os seguintes novos tipos de eventos no watchOS 3:

  • HKWorkoutEventType.Lap - São para eventos que dividem o treino em porções de distância iguais. Por exemplo, para marcar uma volta ao redor de uma pista durante a corrida.
  • HKWorkoutEventType.Marker - São para pontos de interesse arbitrários dentro do treino. Por exemplo, chegar a um ponto específico na rota de uma corrida ao ar livre.

Esses novos tipos podem ser criados pelo aplicativo e armazenados no treino para uso posterior na criação de gráficos e estatísticas.

Para criar um Evento de Marcador, faça o seguinte:

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

Esse código cria uma nova instância de um Evento de Marcador (HKWorkoutEvent) e o salva em uma coleção privada de eventos (que posteriormente será gravada na Sessão de Treino) e notifica o usuário sobre o evento por meio de hápticas.

Pausando e retomando os treinos

A qualquer momento de uma sessão de treino, o usuário pode pausar temporariamente o treino e retomá-lo posteriormente. Por exemplo, eles podem pausar uma corrida interna para atender a uma chamada importante e retomar a execução após a conclusão da chamada.

A interface do usuário do aplicativo deve fornecer uma maneira de pausar e retomar o treino (chamando o HealthKit) para que o Apple Watch possa economizar energia e espaço de dados enquanto o usuário suspende sua atividade. Além disso, o aplicativo deve ignorar quaisquer novos pontos de dados que possam ser recebidos quando a Sessão de Treino estiver em um estado pausado.

O HealthKit responderá a pausar e retomar chamadas gerando eventos Pause e Resume. Enquanto a Sessão de Treino estiver pausada, nenhum novo evento ou dado será enviado ao aplicativo pelo HealthKit até que a sessão seja retomada.

Use o código a seguir para pausar e retomar uma sessão de treino:

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

Os eventos Pause e Resume que serão gerados a partir do HealthKit podem ser manipulados substituindo o DidGenerateEvent método do 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 Movimento

Outra novidade do watchOS 3 são os eventos Motion Paused (HKWorkoutEventType.MotionPaused) e Motion Resumed (HKWorkoutEventType.MotionResumed). Esses eventos são gerados automaticamente pelo HealthKit durante um treino de corrida quando o usuário inicia e para de se mover.

Quando o aplicativo recebe um evento Motion Paused, ele deve parar de coletar dados até que o usuário retome o movimento e o evento Motion Resumes seja recebido. O aplicativo não deve pausar a sessão de Treino em resposta a um evento de Movimento em Pausa.

Importante

Os eventos Motion Paused e Motion Resume são suportados apenas para o Tipo de Atividade RunningWorkout (HKWorkoutActivityType.Running).

Novamente, esses eventos podem ser manipulados substituindo o DidGenerateEvent método do 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;
  }
}

Terminando e salvando a sessão de treino

Quando o usuário tiver concluído seu treino, o aplicativo precisará encerrar a Sessão de Treino atual e salvá-la no banco de dados HealthKit. Os treinos salvos no HealthKit serão exibidos automaticamente na Lista de Atividades de Treino.

Novo no iOS 10, isso inclui a lista de atividades de treino no iPhone do usuário também. Assim, mesmo que o Apple Watch não esteja por perto, o treino será apresentado no telefone.

Os treinos que incluem Amostras de Energia atualizarão o Anel de Movimentação do usuário no aplicativo Atividades para que os aplicativos de terceiros possam agora contribuir para as metas diárias de Movimentação do usuário.

As seguintes etapas são necessárias para encerrar e salvar uma sessão de treino:

Finalizando e salvando o diagrama da sessão de treino

  1. Primeiro, o aplicativo precisará encerrar a Sessão de Treino.
  2. A Sessão de Treino é salva no HealthKit.
  3. Adicione amostras (como energia queimada ou distância) à Sessão de Treino salva.

Encerrando a sessão

Para finalizar a Sessão de Treino, chame o EndWorkoutSession método da HKHealthStore passagem no HKWorkoutSession:

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

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

Isso redefinirá os sensores dos dispositivos para seu modo normal. Quando o HealthKit terminar de terminar o treino, ele receberá um retorno de chamada para o DidChangeToState método do 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;
  }

}

Salvando a sessão

Depois que o aplicativo terminar a Sessão de Treino, ele precisará criar um Treino (HKWorkout) e salvá-lo (junto com um evento) no armazenamento de dados do 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 cria a quantidade total necessária de energia queimada e distância para o treino como HKQuantity objetos. Um dicionário de metadados que definem o treino é criado e o local do treino é especificado:

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

Um novo HKWorkout objeto é criado com o mesmo HKWorkoutActivityType que o HKWorkoutSession, as datas de início e fim, a lista de eventos (sendo acumulada das seções acima), a energia queimada, distância total e dicionário de metadados. Esse objeto é salvo no Repositório de Integridade e quaisquer erros são manipulados.

Adicionando amostras

Quando o aplicativo salva um conjunto de amostras em um Treino, o HealthKit gera uma conexão entre as amostras e o próprio Treino para que o aplicativo possa consultar o HealthKit posteriormente para todas as amostras associadas a um determinado treino. Usando essas informações, o aplicativo pode gerar gráficos a partir dos dados do treino e plotá-los em uma linha do tempo do treino.

Para que um aplicativo contribua para o Move Ring do aplicativo Atividade, ele deve incluir amostras de energia com o treino salvo. Além disso, os totais de distância e energia devem corresponder à soma de todas as amostras que o aplicativo associa a um treino salvo.

Para adicionar amostras a um treino salvo, faça o seguinte:

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, o aplicativo pode calcular e criar um subconjunto menor de amostras ou uma mega amostra (abrangendo toda a faixa do treino) que então é associada ao treino salvo.

Treinos e iOS 10

Cada aplicativo de treino do watchOS 3 tem um aplicativo de treino baseado no iOS 10 e, novidade no iOS 10, este aplicativo do iOS pode ser usado para iniciar um treino que colocará o Apple Watch no Modo de Treino (sem intervenção do usuário) e executará o aplicativo watchOS no modo de Execução em Segundo Plano (consulte Sobre a Execução em Segundo Plano acima para obter mais detalhes).

Enquanto o aplicativo watchOS está em execução, ele pode usar o WatchConnectivity para mensagens e comunicação com o aplicativo iOS pai.

Veja como funciona esse processo:

Diagrama de comunicação do iPhone e Apple Watch

  1. O aplicativo para iPhone cria um HKWorkoutConfiguration objeto e define o Tipo e o Local do Treino.
  2. O HKWorkoutConfiguration objeto é enviado a versão Apple Watch do app e, caso ainda não esteja em execução, é iniciado pelo sistema.
  3. Usando a aprovação na Configuração de Treino, o aplicativo watchOS 3 inicia uma nova Sessão de Treino (HKWorkoutSession).

Importante

Para que o aplicativo pai do iPhone inicie um treino no Apple Watch, o aplicativo watchOS 3 deve ter a Execução em segundo plano ativada. Consulte Ativando a execução em segundo plano acima para obter mais detalhes.

Este processo é muito semelhante ao processo de iniciar uma sessão de treino no aplicativo watchOS 3 diretamente. No iPhone, use o seguinte código:

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 garante que a versão watchOS do aplicativo esteja instalada e a versão do iPhone possa se conectar a ele primeiro:

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

Em seguida, ele cria um HKWorkoutConfiguration como de costume e usa o HKHealthStoreStartWatchApp método do para enviá-lo para o Apple Watch e iniciar o aplicativo e a sessão de treino.

E no aplicativo Watch OS, use o seguinte código no 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);
}

Ele pega o HKWorkoutConfiguration e cria um novo HKWorkoutSession e anexa uma instância do personalizado HKWorkoutSessionDelegate. A Sessão de Treino é iniciada no HealthKit Health Store do usuário.

Juntando todas as peças

Tomando todas as informações apresentadas neste documento, um aplicativo de treino baseado no watchOS 3 e seu aplicativo de treino baseado no iOS 10 pai podem incluir as seguintes partes:

  1. iOS 10 ViewController.cs - Inicia uma sessão de Conectividade do Watch e um treino no Apple Watch.
  2. watchOS 3 ExtensionDelegate.cs - Manipula a versão watchOS 3 do aplicativo de treino.
  3. watchOS 3 OutdoorRunDelegate.cs - Um personalizado HKWorkoutSessionDelegate para lidar com eventos para o treino.

Importante

O código mostrado nas seções a seguir inclui apenas as partes necessárias para implementar os novos recursos aprimorados fornecidos aos aplicativos de treino no watchOS 3. Todo o código de suporte e o código para apresentar e atualizar a interface do usuário não estão incluídos, mas podem ser facilmente criados seguindo nossa outra documentação do watchOS.

ViewController.cs

O ViewController.cs arquivo na versão pai do iOS 10 do aplicativo de treino incluiria o seguinte código:

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

O ExtensionDelegate.cs arquivo na versão watchOS 3 do aplicativo de treino incluiria o seguinte código:

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

O OutdoorRunDelegate.cs arquivo na versão watchOS 3 do aplicativo de treino incluiria o seguinte código:

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

Práticas Recomendadas

A Apple sugere o uso das seguintes práticas recomendadas ao projetar e implementar aplicativos de treino no watchOS 3 e iOS 10:

  • Certifique-se de que o aplicativo watchOS 3 Workout ainda esteja funcional mesmo quando não conseguir se conectar ao iPhone e à versão iOS 10 do aplicativo.
  • Use a distância do HealthKit quando o GPS não estiver disponível, pois é capaz de gerar amostras de distância sem GPS.
  • Permita que o usuário inicie o treino a partir do Apple Watch ou do iPhone.
  • Permita que o aplicativo exiba exercícios de outras fontes (como outros aplicativos de terceiros 3rd) em suas exibições de dados históricos.
  • Certifique-se de que o aplicativo não exiba treinos excluídos em dados históricos.

Resumo

Este artigo abordou as melhorias que a Apple fez para treinar aplicativos no watchOS 3 e como implementá-los no Xamarin.