다음을 통해 공유


Xamarin의 watchOS 운동 앱

이 문서에서는 Apple이 watchOS 3에서 앱을 운동하기 위해 만든 향상된 기능과 Xamarin에서 이를 구현하는 방법에 대해 설명합니다.

watchOS 3을 새롭게 접하는 운동 관련 앱은 Apple Watch에서 백그라운드에서 실행되고 HealthKit 데이터에 액세스할 수 있습니다. 부모 iOS 10 기반 앱은 사용자 개입 없이 watchOS 3 기반 앱을 시작하는 기능도 있습니다.

다음 항목에 대해 자세히 설명합니다.

운동 앱 정보

피트니스 및 운동 앱 사용자는 건강과 피트니스 목표를 향해 하루 중 몇 시간 동안 헌신할 수 있습니다. 따라서 데이터를 정확하게 수집 및 표시하고 Apple Health와 원활하게 통합하는 응답성이 뛰어나고 사용하기 쉬운 앱을 기대합니다.

잘 디자인 된 피트니스 또는 운동 응용 프로그램은 사용자가 자신의 피트니스 목표를 달성하기 위해 자신의 활동을 차트하는 데 도움이됩니다. Apple Watch를 사용하여 피트니스 및 운동 앱은 심박수, 칼로리 연소 및 활동 감지에 즉시 액세스할 수 있습니다.

피트니스 및 운동 앱 예제

watchOS 3의 새로운 백그라운드 실행 은 운동 관련 앱이 Apple Watch의 백그라운드에서 실행되고 HealthKit 데이터에 액세스할 수 있는 기능을 제공합니다.

이 문서에서는 백그라운드 실행 기능을 소개하고, 운동 앱 수명 주기를 다루며, 운동 앱이 Apple Watch에서 사용자의 활동 링 에 기여할 수 있는 방법을 보여줍니다.

운동 세션 정보

모든 운동 앱의 핵심은 사용자가 시작하고 중지할 수 있는 운동 세션(HKWorkoutSession)입니다. 운동 세션 API는 구현하기 쉽고 다음과 같은 운동 앱에 몇 가지 이점을 제공합니다.

  • 동작 유형에 따른 동작 및 칼로리 소모 감지.
  • 사용자의 활동 링에 자동으로 기여합니다.
  • 세션에 있는 동안 사용자가 장치를 깨울 때마다(손목을 올리거나 Apple Watch와 상호 작용하여) 앱이 자동으로 표시됩니다.

백그라운드 실행 정보

위에서 설명한 대로 watchOS 3을 사용하면 운동 앱을 백그라운드에서 실행하도록 설정할 수 있습니다. 백그라운드 실행 운동 앱을 사용하면 백그라운드에서 실행되는 동안 Apple Watch의 센서에서 데이터를 처리할 수 있습니다. 예를 들어 앱은 화면에 더 이상 표시되지 않더라도 사용자의 심박수를 계속 모니터링할 수 있습니다.

백그라운드 실행은 활성 운동 세션 중에 언제든지 사용자에게 라이브 피드백을 제공하는 기능(예: 사용자에게 현재 진행 상황을 알리는 촉각 경고 보내기)을 제공합니다.

또한 백그라운드 실행을 사용하면 사용자가 Apple Watch를 빠르게 볼 때 최신 데이터를 사용할 수 있도록 앱에서 사용자 인터페이스를 빠르게 업데이트할 수 있습니다.

Apple Watch에서 고성능을 기본 위해 백그라운드 실행을 사용하는 시계 앱은 배터리를 절약하기 위해 백그라운드 작업의 양을 제한해야 합니다. 앱이 백그라운드에서 과도한 CPU를 사용하는 경우 watchOS에서 일시 중단될 수 있습니다.

백그라운드 실행 사용

백그라운드 실행을 사용하도록 설정하려면 다음을 수행합니다.

  1. 솔루션 탐색기 Watch 확장의 도우미 i전화 앱 파일을 Info.plist 두 번 클릭하여 편집용으로 엽니다.

  2. 원본 보기로 전환합니다.

    원본 뷰

  3. 호출 WKBackgroundModes 된 새 키를 추가하고 형식을 다음으로 Array설정합니다.

    WKBackgroundModes라는 새 키 추가

  4. 형식String 및 값workout-processing이 다음인 배열에 새 항목을 추가합니다.

    문자열 유형 및 운동 처리 값을 사용하여 배열에 새 항목 추가

  5. 변경 내용을 파일에 저장합니다.

운동 세션 시작

운동 세션을 시작하는 세 가지 기본 단계가 있습니다.

운동 세션을 시작하는 세 가지 기본 단계

  1. 앱은 HealthKit의 데이터에 액세스하기 위한 권한 부여를 요청해야 합니다.
  2. 시작 중인 운동 유형에 대한 운동 구성 개체를 만듭니다.
  3. 새로 만든 운동 구성을 사용하여 운동 세션을 만들고 시작합니다.

권한 부여 요청

앱이 사용자의 HealthKit 데이터에 액세스하려면 먼저 사용자로부터 권한 부여를 요청하고 받아야 합니다. 운동 앱의 특성에 따라 다음과 같은 유형의 요청을 수행할 수 있습니다.

  • 데이터를 쓸 수 있는 권한 부여:
    • 운동
  • 데이터를 읽을 수 있는 권한 부여:
    • 에너지 소모
    • 거리
    • 심박수

앱이 권한 부여를 요청하려면 HealthKit에 액세스하도록 구성해야 합니다.

다음을 수행하십시오:

  1. 편집하기 위해 솔루션 탐색기에서 Entitlements.plist 파일을 두 번 클릭하여 엽니다.

  2. 아래쪽으로 스크롤하여 HealthKit 사용 검사.

    HealthKit 사용 확인

  3. 변경 내용을 파일에 저장합니다.

  4. 명시적 앱 ID 및 프로비저닝 프로필 지침에 따라 앱 ID 및 프로비저닝 프로필을 HealthKit 소개 문서의 Xamarin.iOS 앱 섹션과 연결하여 앱을 올바르게 프로비전합니다.

  5. 마지막으로 HealthKit 소개 문서의 사용자 섹션에서 프로그래밍 상태 키트권한 요청 섹션의 지침을 사용하여 사용자의 HealthKit 데이터 저장소에 액세스하기 위한 권한 부여를 요청합니다.

운동 구성 설정

운동 세션은 운동 유형(예: )과 운동 위치(HKWorkoutConfiguration예: HKWorkoutActivityType.Running)를 지정하는 운동 구성 개체()를 HKWorkoutSessionLocationType.Outdoor사용하여 생성됩니다.

using HealthKit;
...

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

운동 세션 대리자 만들기

운동 세션 중에 발생할 수 있는 이벤트를 처리하려면 앱에서 운동 세션 대리자 인스턴스를 만들어야 합니다. 프로젝트에 새 클래스를 추가하고 클래스를 기반으로 합니다 HKWorkoutSessionDelegate . 실외 실행의 예는 다음과 같을 수 있습니다.

using System;
using Foundation;
using WatchKit;
using HealthKit;

namespace MonkeyWorkout.MWWatchExtension
{
  public class OutdoorRunDelegate : HKWorkoutSessionDelegate
  {
    #region Computed Properties
    public HKHealthStore HealthStore { get; private set; }
    public HKWorkoutSession WorkoutSession { get; private set;}
    #endregion

    #region Constructors
    public OutdoorRunDelegate (HKHealthStore healthStore, HKWorkoutSession workoutSession)
    {
      // Initialize
      this.HealthStore = healthStore;
      this.WorkoutSession = workoutSession;

      // Attach this delegate to the session
      workoutSession.Delegate = this;
    }
    #endregion

    #region Override Methods
    public override void DidFail (HKWorkoutSession workoutSession, NSError error)
    {
      // Handle workout session failing
      RaiseFailed ();
    }

    public override void DidChangeToState (HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date)
    {
      // Take action based on the change in state
      switch (toState) {
      case HKWorkoutSessionState.NotStarted:
        break;
      case HKWorkoutSessionState.Paused:
        RaisePaused ();
        break;
      case HKWorkoutSessionState.Running:
        RaiseRunning ();
        break;
      case HKWorkoutSessionState.Ended:
        RaiseEnded ();
        break;
      }

    }

    public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
    {
      base.DidGenerateEvent (workoutSession, @event);
    }
    #endregion

    #region Events
    public delegate void OutdoorRunEventDelegate ();

    public event OutdoorRunEventDelegate Failed;
    internal void RaiseFailed ()
    {
      if (this.Failed != null) this.Failed ();
    }

    public event OutdoorRunEventDelegate Paused;
    internal void RaisePaused ()
    {
      if (this.Paused != null) this.Paused ();
    }

    public event OutdoorRunEventDelegate Running;
    internal void RaiseRunning ()
    {
      if (this.Running != null) this.Running ();
    }

    public event OutdoorRunEventDelegate Ended;
    internal void RaiseEnded ()
    {
      if (this.Ended != null) this.Ended ();
    }
    #endregion
  }
}

이 클래스는 운동 세션의 상태가 변경되고() 운동 세션이 실패하는 경우(DidChangeToStateDidFail)로 발생하는 여러 이벤트를 만듭니다.

운동 세션 만들기

위에서 만든 운동 구성 및 운동 세션 대리자를 사용하여 새 운동 세션을 만들고 사용자의 기본 HealthKit 저장소에 대해 시작합니다.

using HealthKit;
...

#region Computed Properties
public HKHealthStore HealthStore { get; set;} = new HKHealthStore ();
public OutdoorRunDelegate RunDelegate { get; set; }
#endregion
...

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

  // Create workout session
  // Start workout session
  NSError error = null;
  var workoutSession = new HKWorkoutSession (configuration, out error);

  // Successful?
  if (error != null) {
    // Report error to user and return
    return;
  }

  // Create workout session delegate and wire-up events
  RunDelegate = new OutdoorRunDelegate (HealthStore, workoutSession);

  RunDelegate.Failed += () => {
    // Handle the session failing
  };

  RunDelegate.Paused += () => {
    // Handle the session being paused
  };

  RunDelegate.Running += () => {
    // Handle the session running
  };

  RunDelegate.Ended += () => {
    // Handle the session ending
  };

  // Start session
  HealthStore.StartWorkoutSession (workoutSession);
}

앱이 이 운동 세션을 시작하고 사용자가 시계 얼굴로 다시 전환하면 작은 녹색 "런닝맨" 아이콘이 얼굴 위에 표시됩니다.

얼굴 위에 표시되는 작은 녹색 달리기 남자 아이콘

사용자가 이 아이콘을 탭하면 앱으로 다시 이동됩니다.

데이터 수집 및 제어

운동 세션이 구성되고 시작되면 앱은 세션에 대한 데이터(예: 사용자의 심박수)를 수집하고 세션 상태를 제어해야 합니다.

데이터 수집 및 제어 다이어그램

  1. 샘플 관찰 - 앱은 HealthKit에서 사용자에게 동작하고 표시할 정보를 검색해야 합니다.
  2. 이벤트 관찰 - 앱은 HealthKit 또는 앱의 UI에서 생성된 이벤트(예: 운동을 일시 중지하는 사용자)에 응답해야 합니다.
  3. 실행 중 상태 입력 - 세션이 시작되었으며 현재 실행 중입니다.
  4. 일시 중지됨 상태 입력 - 사용자가 현재 운동 세션을 일시 중지했으며 나중에 다시 시작할 수 있습니다. 사용자는 단일 운동 세션에서 실행 중 상태와 일시 중지된 상태 간에 여러 번 전환할 수 있습니다.
  5. 운동 세션 종료 - 언제든지 사용자가 운동 세션을 종료하거나 계량 운동 (예 : 2 마일 달리기)인 경우 만료되어 자체적으로 끝날 수 있습니다.

마지막 단계는 사용자의 HealthKit 데이터 저장소에 운동 세션의 결과를 저장하는 것입니다.

HealthKit 샘플 관찰

앱은 심박수 또는 활성 에너지 소모와 같이 관심 있는 각 HealthKit 데이터 포인트에 대한 앵커 개체 쿼리를 열어야 합니다. 관찰되는 각 데이터 요소에 대해 앱으로 전송될 때 새 데이터를 캡처하기 위해 업데이트 처리기를 만들어야 합니다.

이러한 데이터 요소에서 앱은 합계(예: 총 실행 거리)를 누적하고 필요에 따라 사용자 인터페이스를 업데이트할 수 있습니다. 또한 앱은 특정 목표 또는 도전 과제(예: 실행의 다음 마일 완료)에 도달했을 때 사용자에게 알릴 수 있습니다.

다음 샘플 코드를 살펴보세요.

private void ObserveHealthKitSamples ()
{
  // Get the starting date of the required samples
  var datePredicate = HKQuery.GetPredicateForSamples (WorkoutSession.StartDate, null, HKQueryOptions.StrictStartDate);

  // Get data from the local device
  var devices = new NSSet<HKDevice> (new HKDevice [] { HKDevice.LocalDevice });
  var devicePredicate = HKQuery.GetPredicateForObjectsFromDevices (devices);

  // Assemble compound predicate
  var queryPredicate = NSCompoundPredicate.CreateAndPredicate (new NSPredicate [] { datePredicate, devicePredicate });

  // Get ActiveEnergyBurned
  var queryActiveEnergyBurned = new HKAnchoredObjectQuery (HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned), queryPredicate, null, HKSampleQuery.NoLimit, (query, addedObjects, deletedObjects, newAnchor, error) => {
    // Valid?
    if (error == null) {
      // Yes, process all returned samples
      foreach (HKSample sample in addedObjects) {
        var quantitySample = sample as HKQuantitySample;
        ActiveEnergyBurned += quantitySample.Quantity.GetDoubleValue (HKUnit.Joule);
      }

      // Update User Interface
      ...
    }
  });

  // Start Query
  HealthStore.ExecuteQuery (queryActiveEnergyBurned);

}

메서드를 사용하기 위한 데이터를 가져올 시작 날짜를 설정하는 조건자를 GetPredicateForSamples 만듭니다. 이 메서드를 사용하여 GetPredicateForObjectsFromDevices HealthKit 정보를 가져오는 디바이스 집합을 만듭니다. 이 경우 로컬 Apple Watch만(HKDevice.LocalDevice)입니다. 두 조건자는 메서드를 사용하여 CreateAndPredicate 복합 조건자(NSCompoundPredicate)로 결합됩니다.

원하는 데이터 요소(이 경우 Active Energy Burned 데이터 요소의 경우HKQuantityTypeIdentifier.ActiveEnergyBurned)에 대한 새 HKAnchoredObjectQuery 항목이 만들어지고, 반환되는 데이터 양(HKSampleQuery.NoLimit)에 제한이 부과되지 않으며, HealthKit에서 앱으로 반환되는 데이터를 처리하도록 업데이트 처리기가 정의됩니다.

지정된 데이터 요소에 대한 새 데이터가 앱에 배달될 때마다 업데이트 처리기가 호출됩니다. 오류가 반환되지 않으면 앱은 데이터를 안전하게 읽고, 필요한 계산을 수행하고, 필요에 따라 UI를 업데이트할 수 있습니다.

코드는 배열에서 addedObjects 반환된 모든 샘플(HKSample)을 반복하여 수량 샘플(HKQuantitySample)로 캐스팅합니다. 그런 다음 샘플의 이중 값을 joule(HKUnit.Joule)로 가져오고 운동을 위해 연소된 활성 에너지의 실행 합계에 누적하고 사용자 인터페이스를 업데이트합니다.

달성된 목표 알림

위에서 멘션 사용자가 운동 앱에서 목표를 달성하면(예: 실행의 첫 번째 마일 완료) Taptic 엔진을 통해 사용자에게 촉각 피드백을 보낼 수 있습니다. 사용자가 피드백을 유도하는 이벤트를 보기 위해 손목을 올릴 가능성이 높기 때문에 앱은 이 시점에서 UI를 업데이트해야 합니다.

촉각 피드백을 재생하려면 다음 코드를 사용합니다.

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

이벤트 관찰

이벤트는 앱이 사용자의 운동 중 특정 지점을 강조 표시하는 데 사용할 수 있는 타임스탬프입니다. 일부 이벤트는 앱에서 직접 만들어지고 운동에 저장되며 일부 이벤트는 HealthKit에서 자동으로 생성됩니다.

HealthKit에서 만든 이벤트를 관찰하기 위해 앱은 다음의 메서드를 DidGenerateEvent 재정의합니다 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은 watchOS 3에 다음과 같은 새로운 이벤트 유형을 추가했습니다.

  • HKWorkoutEventType.Lap - 운동을 동일한 거리 부분으로 분해하는 이벤트입니다. 예를 들어, 실행하는 동안 트랙 주위에 한 바퀴를 표시합니다.
  • HKWorkoutEventType.Marker - 운동 내에서 관심의 임의의 지점에 대한 것입니다. 예를 들어 야외 실행 경로의 특정 지점에 도달합니다.

이러한 새 형식은 앱에서 만들고 나중에 그래프 및 통계를 만드는 데 사용할 수 있는 운동에 저장할 수 있습니다.

표식 이벤트를 만들려면 다음을 수행합니다.

using System.Collections.Generic;
...

public float MilesRun { get; set; }
public List<HKWorkoutEvent> WorkoutEvents { get; set; } = new List<HKWorkoutEvent> ();
...

public void ReachedNextMile ()
{
  // Create and save marker event
  var markerEvent = HKWorkoutEvent.Create (HKWorkoutEventType.Marker, NSDate.Now);
  WorkoutEvents.Add (markerEvent);

  // Notify user
  NotifyUserOfReachedMileGoal (++MilesRun);
}

이 코드는 마커 이벤트(HKWorkoutEvent)의 새 인스턴스를 만들고 프라이빗 이벤트 컬렉션(나중에 운동 세션에 기록됨)에 저장하고 촉각을 통해 사용자에게 이벤트 정보를 제공합니다.

운동 일시 중지 및 재개

운동 세션의 어느 시점에서, 사용자는 일시적으로 운동을 일시 중지 하 고 나중에 다시 시작할 수 있습니다. 예를 들어 실내 실행을 일시 중지하여 중요한 호출을 수행하고 호출이 완료된 후 실행을 다시 시작할 수 있습니다.

앱의 UI는 사용자가 활동을 일시 중단하는 동안 Apple Watch가 전원 및 데이터 공간을 모두 절약 할 수 있도록 운동을 일시 중지하고 다시 시작하는 방법을 제공해야합니다(HealthKit로 호출). 또한 앱은 운동 세션이 일시 중지된 상태일 때 수신될 수 있는 새 데이터 요소를 무시해야 합니다.

HealthKit는 일시 중지 및 다시 시작 이벤트를 생성하여 일시 중지 및 다시 시작 호출에 응답합니다. 운동 세션이 일시 중지된 동안 세션이 다시 시작될 때까지 HealthKit에서 앱으로 새 이벤트 또는 데이터를 보내지 않습니다.

다음 코드를 사용하여 운동 세션을 일시 중지하고 다시 시작합니다.

public HKHealthStore HealthStore { get; set;} = new HKHealthStore ();
public HKWorkoutSession WorkoutSession { get; set;}
...

public void PauseWorkout ()
{
  // Pause the current workout
  HealthStore.PauseWorkoutSession (WorkoutSession);
}

public void ResumeWorkout ()
{
  // Pause the current workout
  HealthStore.ResumeWorkoutSession (WorkoutSession);
}

HealthKit에서 생성되는 일시 중지 및 다시 시작 이벤트는 다음의 메서드를 재정의HKWorkoutSessionDelegate하여 DidGenerateEvent 처리할 수 있습니다.

public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
{
  base.DidGenerateEvent (workoutSession, @event);

  // Take action based on the type of event
  switch (@event.Type) {
  case HKWorkoutEventType.Pause:
    break;
  case HKWorkoutEventType.Resume:
    break;
  }
}

동작 이벤트

또한 watchOS 3의 새로운 기능으로 동작 일시 중지(HKWorkoutEventType.MotionPaused) 및 동작 다시 시작(HKWorkoutEventType.MotionResumed) 이벤트가 있습니다. 이러한 이벤트는 사용자가 이동을 시작하고 중지할 때 실행 중인 운동 중에 HealthKit에 의해 자동으로 발생합니다.

앱이 동작 일시 중지 이벤트를 받으면 사용자가 동작을 다시 시작하고 Motion Resumes 이벤트가 수신될 때까지 데이터 수집을 중지해야 합니다. 앱은 동작 일시 중지 이벤트에 대한 응답으로 운동 세션을 일시 중지하지 않아야 합니다.

Important

동작 일시 중지 및 동작 다시 시작 이벤트는 RunningWorkout 활동 유형(HKWorkoutActivityType.Running)에 대해서만 지원됩니다.

다시 말하지만, 다음의 메서드를 재정의HKWorkoutSessionDelegate하여 DidGenerateEvent 이러한 이벤트를 처리할 수 있습니다.

public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
{
  base.DidGenerateEvent (workoutSession, @event);

  // Take action based on the type of event
  switch (@event.Type) {
  case HKWorkoutEventType.MotionPaused:
    break;
  case HKWorkoutEventType.MotionResumed:
    break;
  }
}

운동 세션 종료 및 저장

사용자가 운동을 완료하면 앱은 현재 운동 세션을 종료하고 HealthKit 데이터베이스에 저장해야 합니다. HealthKit에 저장된 운동은 운동 활동 목록에 자동으로 표시됩니다.

iOS 10의 새로운 기능으로 사용자의 i전화 운동 활동 목록 목록도 포함됩니다. 그래서 애플 시계 근처에 없는 경우에, 운동 전화에 제시 될 것 이다.

에너지 샘플을 포함하는 운동은 이제 타사 앱이 사용자의 일일 이동 목표에 기여할 수 있도록 활동 앱에서 사용자의 이동 링을 업데이트합니다.

다음 단계는 연습 세션을 종료하고 저장하는 데 필요합니다.

운동 세션 다이어그램 종료 및 저장

  1. 첫째, 앱은 운동 세션을 종료해야 합니다.
  2. 운동 세션은 HealthKit에 저장됩니다.
  3. 저장된 운동 세션에 샘플(예: 연소된 에너지 또는 거리)을 추가합니다.

세션 종료

운동 세션을 종료하려면 다음을 전달하는 메서드를 HKHealthStore 호출 EndWorkoutSession 합니다HKWorkoutSession.

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

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

이렇게 하면 디바이스 센서가 정상 모드로 다시 설정됩니다. HealthKit이 운동 종료를 마치면 다음 메서드에 대한 콜백을 DidChangeToStateHKWorkoutSessionDelegate받게 됩니다.

public override void DidChangeToState (HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date)
{
  // Take action based on the change in state
  switch (toState) {
  ...
  case HKWorkoutSessionState.Ended:
    StopObservingHealthKitSamples ();
    RaiseEnded ();
    break;
  }

}

세션 저장

앱이 운동 세션을 종료하면 운동(HKWorkout)을 만들고 HealthKit 데이터 저장소(HKHealthStore이벤트와 함께)에 저장해야 합니다.

public HKHealthStore HealthStore { get; private set; }
public HKWorkoutSession WorkoutSession { get; private set;}
public float MilesRun { get; set; }
public double ActiveEnergyBurned { get; set;}
public List<HKWorkoutEvent> WorkoutEvents { get; set; } = new List<HKWorkoutEvent> ();
...

private void SaveWorkoutSession ()
{
  // Build required workout quantities
  var energyBurned = HKQuantity.FromQuantity (HKUnit.Joule, ActiveEnergyBurned);
  var distance = HKQuantity.FromQuantity (HKUnit.Mile, MilesRun);

  // Create any required metadata
  var metadata = new NSMutableDictionary ();
  metadata.Add (new NSString ("HKMetadataKeyIndoorWorkout"), new NSString ("NO"));

  // Create workout
  var workout = HKWorkout.Create (HKWorkoutActivityType.Running,
                                  WorkoutSession.StartDate,
                                  NSDate.Now,
                                  WorkoutEvents.ToArray (),
                                  energyBurned,
                                  distance,
                                  metadata);

  // Save to HealthKit
  HealthStore.SaveObject (workout, (successful, error) => {
    // Handle any errors
    if (error == null) {
      // Was the save successful
      if (successful) {

      }
    } else {
      // Report error
    }
  });

}

이 코드는 총 에너지 소모량과 운동 거리를 개체로 HKQuantity 만듭니다. 운동을 정의하는 메타데이터 사전이 생성되고 운동의 위치가 지정됩니다.

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

HKWorkout 개체는 시작 및 종료 날짜, 이벤트 목록(위 섹션에서 누적됨), 연소된 에너지, 총 거리 및 메타데이터 사전과 동일하게 HKWorkoutActivityTypeHKWorkoutSession만들어집니다. 이 개체는 Health Store에 저장되고 오류가 처리됩니다.

샘플 추가

앱이 샘플 집합을 연습에 저장하면 HealthKit는 샘플과 운동 자체 간의 연결을 생성하므로 앱은 지정된 운동과 관련된 모든 샘플에 대해 나중에 HealthKit을 쿼리할 수 있습니다. 이 정보를 사용하여 앱은 운동 데이터에서 그래프를 생성하고 운동 타임라인 대해 그래프를 그릴 수 있습니다.

앱이 활동 앱의 이동 링에 기여하려면 저장된 운동과 함께 에너지 샘플을 포함해야 합니다. 또한 거리 및 에너지의 합계는 앱이 저장된 운동과 연결하는 샘플의 합계와 일치해야 합니다.

저장된 운동에 샘플을 추가하려면 다음을 수행합니다.

using System.Collections.Generic;
using WatchKit;
using HealthKit;
...

public HKHealthStore HealthStore { get; private set; }
public List<HKSample> WorkoutSamples { get; set; } = new List<HKSample> ();
...

private void SaveWorkoutSamples (HKWorkout workout)
{
  // Add samples to saved workout
  HealthStore.AddSamples (WorkoutSamples.ToArray (), workout, (success, error) => {
    // Handle any errors
    if (error == null) {
      // Was the save successful
      if (success) {

      }
    } else {
      // Report error
    }
  });
}

필요에 따라 앱은 더 작은 샘플 하위 집합 또는 하나의 메가 샘플(운동의 전체 범위에 걸쳐 있는)을 계산하고 만들 수 있으며, 이 샘플은 저장된 운동과 연결됩니다.

운동 및 iOS 10

모든 watchOS 3 운동 앱에는 부모 iOS 10 기반 운동 앱이 있으며 iOS 10을 처음으로 사용하는 이 iOS 앱을 사용하여 Apple Watch를 운동 모드(사용자 개입 없이)에 배치하고 백그라운드 실행 모드에서 watchOS 앱을 실행하는 운동을 시작할 수 있습니다(자세한 내용은 위의 백그라운드 실행 정보 참조).

watchOS 앱이 실행되는 동안에는 부모 iOS 앱과의 메시징 및 통신에 Watch커넥트ivity를 사용할 수 있습니다.

이 프로세스의 작동 방식을 살펴보세요.

i전화 및 Apple Watch 통신 다이어그램

  1. i전화 앱은 개체를 HKWorkoutConfiguration 만들고 운동 유형 및 위치를 설정합니다.
  2. 개체는 HKWorkoutConfiguration 앱의 Apple Watch 버전으로 전송되며, 아직 실행되고 있지 않으면 시스템에서 시작됩니다.
  3. watchOS 3 앱은 통과된 운동 구성을 사용하여 새 운동 세션(HKWorkoutSession)을 시작합니다.

Important

부모 i전화 앱이 Apple Watch에서 운동을 시작하려면 watchOS 3 앱에 백그라운드 실행을 사용하도록 설정해야 합니다. 자세한 내용은 위에서 실행 중인 백그라운드 사용 설정을 참조하세요.

이 프로세스는 watchOS 3 앱에서 직접 운동 세션을 시작하는 프로세스와 매우 유사합니다. i전화 다음 코드를 사용합니다.

using System;
using HealthKit;
using WatchConnectivity;
...

#region Computed Properties
public HKHealthStore HealthStore { get; set; } = new HKHealthStore ();
public WCSession ConnectivitySession { get; set; } = WCSession.DefaultSession;
#endregion
...

private void StartOutdoorRun ()
{
  // Can the app communicate with the watchOS version of the app?
  if (ConnectivitySession.ActivationState == WCSessionActivationState.Activated && ConnectivitySession.WatchAppInstalled) {
    // Create a workout configuration
    var configuration = new HKWorkoutConfiguration () {
      ActivityType = HKWorkoutActivityType.Running,
      LocationType = HKWorkoutSessionLocationType.Outdoor
    };

    // Start watch app
    HealthStore.StartWatchApp (configuration, (success, error) => {
      // Handle any errors
      if (error == null) {
        // Was the save successful
        if (success) {
          ...
        }
      } else {
        // Report error
        ...
      }
    });
  }
}

이 코드는 앱의 watchOS 버전이 설치되고 i전화 버전이 먼저 앱에 연결할 수 있도록 합니다.

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

그런 다음 평소와 같이 만들고 HKWorkoutConfiguration Apple Watch에 보내고 앱과 운동 세션을 시작하는 방법을 HKHealthStore 사용합니다StartWatchApp.

또한 조사식 OS 앱에서 다음 코드를 사용합니다.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);
}

새 인스턴스를 HKWorkoutConfigurationHKWorkoutSession 가져와서 만들고 사용자 지정 HKWorkoutSessionDelegate인스턴스를 연결합니다. 사용자의 HealthKit Health Store에 대해 운동 세션이 시작됩니다.

모든 조각을 함께 가져오기

이 문서에 제시된 모든 정보를 사용하여 watchOS 3 기반 운동 앱과 부모 iOS 10 기반 운동 앱은 다음 부분을 포함할 수 있습니다.

  1. iOS 10 ViewController.cs - Watch 커넥트ivity 세션의 시작과 Apple Watch의 운동을 처리합니다.
  2. watchOS 3 ExtensionDelegate.cs - watchOS 3 버전의 운동 앱을 처리합니다.
  3. watchOS 3 OutdoorRunDelegate.cs - 운동을 위한 이벤트를 처리하는 사용자 지정 HKWorkoutSessionDelegate 입니다.

Important

다음 섹션에 표시된 코드에는 watchOS 3의 운동 앱에 제공되는 새로운 향상된 기능을 구현하는 데 필요한 부분만 포함됩니다. UI를 표시하고 업데이트하는 모든 지원 코드와 코드는 포함되지 않지만 다른 watchOS 설명서에 따라 쉽게 만들 수 있습니다.

ViewController.cs

부모 iOS 10 버전의 운동 앱에 있는 파일에는 ViewController.cs 다음 코드가 포함됩니다.

using System;
using HealthKit;
using UIKit;
using WatchConnectivity;

namespace MonkeyWorkout
{
  public partial class ViewController : UIViewController
  {
    #region Computed Properties
    public HKHealthStore HealthStore { get; set; } = new HKHealthStore ();
    public WCSession ConnectivitySession { get; set; } = WCSession.DefaultSession;
    #endregion

    #region Constructors
    protected ViewController (IntPtr handle) : base (handle)
    {
      // Note: this .ctor should not contain any initialization logic.
    }
    #endregion

    #region Private Methods
    private void InitializeWatchConnectivity ()
    {
      // Is Watch Connectivity supported?
      if (!WCSession.IsSupported) {
        // No, abort
        return;
      }

      // Is the session already active?
      if (ConnectivitySession.ActivationState != WCSessionActivationState.Activated) {
        // No, start session
        ConnectivitySession.ActivateSession ();
      }
    }

    private void StartOutdoorRun ()
    {
      // Can the app communicate with the watchOS version of the app?
      if (ConnectivitySession.ActivationState == WCSessionActivationState.Activated && ConnectivitySession.WatchAppInstalled) {
        // Create a workout configuration
        var configuration = new HKWorkoutConfiguration () {
          ActivityType = HKWorkoutActivityType.Running,
          LocationType = HKWorkoutSessionLocationType.Outdoor
        };

        // Start watch app
        HealthStore.StartWatchApp (configuration, (success, error) => {
          // Handle any errors
          if (error == null) {
            // Was the save successful
            if (success) {
              ...
            }
          } else {
            // Report error
            ...
          }
        });
      }
    }
    #endregion

    #region Override Methods
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();

      // Start Watch Connectivity
      InitializeWatchConnectivity ();
    }
    #endregion
  }
}

ExtensionDelegate.cs

watchOS 3 버전의 운동 앱 파일에는 ExtensionDelegate.cs 다음 코드가 포함됩니다.

using System;
using Foundation;
using WatchKit;
using HealthKit;

namespace MonkeyWorkout.MWWatchExtension
{
  public class ExtensionDelegate : WKExtensionDelegate
  {
    #region Computed Properties
    public HKHealthStore HealthStore { get; set;} = new HKHealthStore ();
    public OutdoorRunDelegate RunDelegate { get; set; }
    #endregion

    #region Constructors
    public ExtensionDelegate ()
    {

    }
    #endregion

    #region Private Methods
    private void StartWorkoutSession (HKWorkoutConfiguration workoutConfiguration)
    {
      // Create workout session
      // Start workout session
      NSError error = null;
      var workoutSession = new HKWorkoutSession (workoutConfiguration, out error);

      // Successful?
      if (error != null) {
        // Report error to user and return
        return;
      }

      // Create workout session delegate and wire-up events
      RunDelegate = new OutdoorRunDelegate (HealthStore, workoutSession);

      RunDelegate.Failed += () => {
        // Handle the session failing
        ...
      };

      RunDelegate.Paused += () => {
        // Handle the session being paused
        ...
      };

      RunDelegate.Running += () => {
        // Handle the session running
        ...
      };

      RunDelegate.Ended += () => {
        // Handle the session ending
        ...
      };

      RunDelegate.ReachedMileGoal += (miles) => {
        // Handle the reaching a session goal
        ...
      };

      RunDelegate.HealthKitSamplesUpdated += () => {
        // Update UI as required
        ...
      };

      // Start session
      HealthStore.StartWorkoutSession (workoutSession);
    }

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

      // Start the session
      StartWorkoutSession (workoutConfiguration);
    }
    #endregion

    #region Override Methods
    public override void HandleWorkoutConfiguration (HKWorkoutConfiguration workoutConfiguration)
    {
      // Start the session
      StartWorkoutSession (workoutConfiguration);
    }
    #endregion
  }
}

OutdoorRunDelegate.cs

watchOS 3 버전의 운동 앱 파일에는 OutdoorRunDelegate.cs 다음 코드가 포함됩니다.

using System;
using System.Collections.Generic;
using Foundation;
using WatchKit;
using HealthKit;

namespace MonkeyWorkout.MWWatchExtension
{
  public class OutdoorRunDelegate : HKWorkoutSessionDelegate
  {
    #region Private Variables
    private HKAnchoredObjectQuery QueryActiveEnergyBurned;
    #endregion

    #region Computed Properties
    public HKHealthStore HealthStore { get; private set; }
    public HKWorkoutSession WorkoutSession { get; private set;}
    public float MilesRun { get; set; }
    public double ActiveEnergyBurned { get; set;}
    public List<HKWorkoutEvent> WorkoutEvents { get; set; } = new List<HKWorkoutEvent> ();
    public List<HKSample> WorkoutSamples { get; set; } = new List<HKSample> ();
    #endregion

    #region Constructors
    public OutdoorRunDelegate (HKHealthStore healthStore, HKWorkoutSession workoutSession)
    {
      // Initialize
      this.HealthStore = healthStore;
      this.WorkoutSession = workoutSession;

      // Attach this delegate to the session
      workoutSession.Delegate = this;

    }
    #endregion

    #region Private Methods
    private void ObserveHealthKitSamples ()
    {
      // Get the starting date of the required samples
      var datePredicate = HKQuery.GetPredicateForSamples (WorkoutSession.StartDate, null, HKQueryOptions.StrictStartDate);

      // Get data from the local device
      var devices = new NSSet<HKDevice> (new HKDevice [] { HKDevice.LocalDevice });
      var devicePredicate = HKQuery.GetPredicateForObjectsFromDevices (devices);

      // Assemble compound predicate
      var queryPredicate = NSCompoundPredicate.CreateAndPredicate (new NSPredicate [] { datePredicate, devicePredicate });

      // Get ActiveEnergyBurned
      QueryActiveEnergyBurned = new HKAnchoredObjectQuery (HKQuantityType.Create (HKQuantityTypeIdentifier.ActiveEnergyBurned), queryPredicate, null, HKSampleQuery.NoLimit, (query, addedObjects, deletedObjects, newAnchor, error) => {
        // Valid?
        if (error == null) {
          // Yes, process all returned samples
          foreach (HKSample sample in addedObjects) {
            // Accumulate totals
            var quantitySample = sample as HKQuantitySample;
            ActiveEnergyBurned += quantitySample.Quantity.GetDoubleValue (HKUnit.Joule);

            // Save samples
            WorkoutSamples.Add (sample);
          }

          // Inform caller
          RaiseHealthKitSamplesUpdated ();
        }
      });

      // Start Query
      HealthStore.ExecuteQuery (QueryActiveEnergyBurned);

    }

    private void StopObservingHealthKitSamples ()
    {
      // Stop query
      HealthStore.StopQuery (QueryActiveEnergyBurned);
    }

    private void ResumeObservingHealthkitSamples ()
    {
      // Resume current queries
      HealthStore.ExecuteQuery (QueryActiveEnergyBurned);
    }

    private void NotifyUserOfReachedMileGoal (float miles)
    {
      // Play haptic feedback
      WKInterfaceDevice.CurrentDevice.PlayHaptic (WKHapticType.Notification);

      // Raise event
      RaiseReachedMileGoal (miles);
    }

    private void SaveWorkoutSession ()
    {
      // Build required workout quantities
      var energyBurned = HKQuantity.FromQuantity (HKUnit.Joule, ActiveEnergyBurned);
      var distance = HKQuantity.FromQuantity (HKUnit.Mile, MilesRun);

      // Create any required metadata
      var metadata = new NSMutableDictionary ();
      metadata.Add (new NSString ("HKMetadataKeyIndoorWorkout"), new NSString ("NO"));

      // Create workout
      var workout = HKWorkout.Create (HKWorkoutActivityType.Running,
                                      WorkoutSession.StartDate,
                                      NSDate.Now,
                                      WorkoutEvents.ToArray (),
                                      energyBurned,
                                      distance,
                                      metadata);

      // Save to HealthKit
      HealthStore.SaveObject (workout, (successful, error) => {
        // Handle any errors
        if (error == null) {
          // Was the save successful
          if (successful) {
            // Add samples to workout
            SaveWorkoutSamples (workout);
          }
        } else {
          // Report error
          ...
        }
      });

    }

    private void SaveWorkoutSamples (HKWorkout workout)
    {
      // Add samples to saved workout
      HealthStore.AddSamples (WorkoutSamples.ToArray (), workout, (success, error) => {
        // Handle any errors
        if (error == null) {
          // Was the save successful
          if (success) {
            ...
          }
        } else {
          // Report error
          ...
        }
      });
    }
    #endregion

    #region Public Methods
    public void PauseWorkout ()
    {
      // Pause the current workout
      HealthStore.PauseWorkoutSession (WorkoutSession);
    }

    public void ResumeWorkout ()
    {
      // Pause the current workout
      HealthStore.ResumeWorkoutSession (WorkoutSession);
    }

    public void ReachedNextMile ()
    {
      // Create and save marker event
      var markerEvent = HKWorkoutEvent.Create (HKWorkoutEventType.Marker, NSDate.Now);
      WorkoutEvents.Add (markerEvent);

      // Notify user
      NotifyUserOfReachedMileGoal (++MilesRun);
    }

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

    #region Override Methods
    public override void DidFail (HKWorkoutSession workoutSession, NSError error)
    {
      // Handle workout session failing
      RaiseFailed ();
    }

    public override void DidChangeToState (HKWorkoutSession workoutSession, HKWorkoutSessionState toState, HKWorkoutSessionState fromState, NSDate date)
    {
      // Take action based on the change in state
      switch (toState) {
      case HKWorkoutSessionState.NotStarted:
        break;
      case HKWorkoutSessionState.Paused:
        StopObservingHealthKitSamples ();
        RaisePaused ();
        break;
      case HKWorkoutSessionState.Running:
        if (fromState == HKWorkoutSessionState.Paused) {
          ResumeObservingHealthkitSamples ();
        } else {
          ObserveHealthKitSamples ();
        }
        RaiseRunning ();
        break;
      case HKWorkoutSessionState.Ended:
        StopObservingHealthKitSamples ();
        SaveWorkoutSession ();
        RaiseEnded ();
        break;
      }

    }

    public override void DidGenerateEvent (HKWorkoutSession workoutSession, HKWorkoutEvent @event)
    {
      base.DidGenerateEvent (workoutSession, @event);

      // Save HealthKit generated event
      WorkoutEvents.Add (@event);

      // Take action based on the type of event
      switch (@event.Type) {
      case HKWorkoutEventType.Lap:
        ...
        break;
      case HKWorkoutEventType.Marker:
        ...
        break;
      case HKWorkoutEventType.MotionPaused:
        ...
        break;
      case HKWorkoutEventType.MotionResumed:
        ...
        break;
      case HKWorkoutEventType.Pause:
        ...
        break;
      case HKWorkoutEventType.Resume:
        ...
        break;
      }
    }
    #endregion

    #region Events
    public delegate void OutdoorRunEventDelegate ();
    public delegate void OutdoorRunMileGoalDelegate (float miles);

    public event OutdoorRunEventDelegate Failed;
    internal void RaiseFailed ()
    {
      if (this.Failed != null) this.Failed ();
    }

    public event OutdoorRunEventDelegate Paused;
    internal void RaisePaused ()
    {
      if (this.Paused != null) this.Paused ();
    }

    public event OutdoorRunEventDelegate Running;
    internal void RaiseRunning ()
    {
      if (this.Running != null) this.Running ();
    }

    public event OutdoorRunEventDelegate Ended;
    internal void RaiseEnded ()
    {
      if (this.Ended != null) this.Ended ();
    }

    public event OutdoorRunMileGoalDelegate ReachedMileGoal;
    internal void RaiseReachedMileGoal (float miles)
    {
      if (this.ReachedMileGoal != null) this.ReachedMileGoal (miles);
    }

    public event OutdoorRunEventDelegate HealthKitSamplesUpdated;
    internal void RaiseHealthKitSamplesUpdated ()
    {
      if (this.HealthKitSamplesUpdated != null) this.HealthKitSamplesUpdated ();
    }
    #endregion
  }
}

좋은 연습 방법

Apple은 watchOS 3 및 iOS 10에서 운동 앱을 디자인하고 구현할 때 다음 모범 사례를 사용하는 것이 좋습니다.

  • i전화 및 iOS 10 버전의 앱에 연결할 수 없는 경우에도 watchOS 3 운동 앱이 여전히 작동하도록 합니다.
  • GPS 없이 거리 샘플을 생성할 수 있으므로 GPS를 사용할 수 없는 경우 HealthKit 거리를 사용합니다.
  • 사용자가 Apple Watch 또는 i전화 운동을 시작할 수 있도록 허용합니다.
  • 앱이 기록 데이터 뷰에 다른 원본(예: 다른 타사 앱)의 운동을 표시하도록 허용합니다.
  • 앱이 기록 데이터에 삭제된 운동을 표시하지 않는지 확인합니다.

요약

이 문서에서는 Apple이 watchOS 3에서 앱을 운동하기 위해 만든 향상된 기능과 Xamarin에서 이를 구현하는 방법을 설명했습니다.