
-
iOS 및 iPadOS에서 HealthKit을 사용하여 운동 기록 추적하기
iOS에서 훌륭한 운동 경험을 구축하는 모범 사례를 알아보세요. 운동 세션의 수명 주기를 검토하고, Apple Watch와 iPhone으로 운동했을 때의 차이점을 알아보며, 실시간 현황 및 Siri를 사용하여 앱의 잠금 화면 경험을 향상시키는 방법을 살펴보세요.
챕터
- 0:00 - 서론
- 0:56 - 운동 세션 실행하기
- 2:50 - 세션 지표 확인하기
- 8:35 - 충돌 후 복구하기
- 9:34 - 모범 사례
리소스
- Building a multidevice workout app
- Building a workout app for iPhone and iPad
- Handling Workout Requests with SiriKit
- HKWorkoutSession
- Running workout sessions
관련 비디오
WWDC25
WWDC24
WWDC23
-
비디오 검색…
안녕하세요 HealthKit 팀의 엔지니어 Brian입니다 사용자의 건강한 생활과 건강을 유지하도록 돕는 수백 개의 건강 및 피트니스 앱이 있습니다 사용자가 동의 시 이러한 앱은 HealthKit의 암호화된 중앙 데이터베이스와 강력한 API에 접근하고 사용자의 건강을 확인할 수 있죠 운동 API는 HealthKit의 강력한 기능 중 하나입니다 iPhone과 iPad에서의 사용 방법을 보여드리죠 우선 운동 세션 실행에 관한 기본 사항을 다루겠습니다 그리고 운동 중 건강 지표에 접근하는 방법을 설명할게요 Apple Watch의 절차와의 차이점에 초점을 두겠습니다 그 후 충돌 발생 시 복구하는 방법과 운동 관련 팁과 모범 사례를 설명하며 마무리하겠습니다 운동 세션을 시작해 보겠습니다 Apple Watch 앱에서 운동을 이미 하고 있다면 iPhone과 iPad에서 같은 코드를 수정해 사용할 수 있죠 Apple Watch처럼 운동 세션에서 활동을 추적하고 관련 운동 빌더로 운동을 HealthKit에 저장할 수 있죠 Apple Watch 앱이 없을 수도 있으니 운동 세션을 실행하는 예시를 빠르게 살펴보겠습니다 운동 세션은 설정 및 시작부터 건강 지표 수집까지 여러 단계로 구성되며 세션 종료로 마무리됩니다 WorkoutConfiguration을 먼저 만들고 사용자의 활동을 반영하기 위해 type을 설정해야 합니다 running을 사용하고 위치는 outdoor로 설정하죠 그리고 configuration을 전달하는 HKWorkoutSession을 만듭니다
운동 세션에서 관련 빌더를 얻은 후 데이터 소스를 추가합니다
그리고 세션에서 prepare를 호출하고 3초 카운트다운을 표시해 온디바이스 센서가 켜지거나 외부 심박수 측정기가 연결될 시간을 주세요 이 카운트다운은 운동 활동이 시작되자마자 지표를 확보할 수 있도록 합니다 카운트다운이 완료되면 세션에서 startActivity를 호출하고 관련 운동 빌더에서 beginCollection을 호출합니다 UI 업데이트를 위해 고정된 객체 쿼리를 사용할 필욘 없죠 workoutBuilder 대리자는 새 데이터 수집 시 앱에 편리한 업데이트를 제공하고 운동이 저장될 때 지표를 동기화합니다
앱 사용자가 운동을 종료하기로 결정하면 세션에서 stopActivity를 호출하여 실시간 빌더가 최종 지표를 수집하도록 합니다
이 세션이 정지된 상태로 전환되면 빌더에서 endCollection을 호출하고 운동을 완료합니다 빌더가 작업을 마치면 세션에서 end를 호출하고 운동 요약 화면을 표시합니다
운동을 실행하는 방법을 알려 드렸으니 Apple Watch에서 운동 실행 절차와 iPhone 또는 iPad에서 실행하는 절차의 차이점을 설명할게요 주요 차이점 중 하나는 사용 가능한 센서입니다
iPhone과 iPad는 모든 운동 활동을 지원하지만 이러한 기기에는 심박수 센서가 내장되어 있지 않습니다
대신 운동 중 착용하며 심박수 GAT 프로필을 지원하는 기기와 페어링할 수 있습니다 웨어러블 심박수 측정기나 Powerbeats Pro 2처럼요
기기가 페어링되면 HealthKit이 기기로부터 심박수 데이터를 수집하고 앱에서 사용할 수 있도록 HealthStore에 샘플로 저장합니다 즉, 운동을 위해 수집하려는 샘플과 시스템이 생성할 수 있는 샘플 간에 차이가 존재할 수 있습니다 이 차이에 대해 설명할게요
생성된 타입이 있습니다 운동 활동 중에 시스템에서 생성되는 데이터 타입으로 칼로리나 거리 등이 있습니다 수집된 타입도 있죠 이는 실시간으로 모니터링하고 운동 샘플에 추가할 지표입니다 예를 들어, 사용자가 운동 중 섭취한 물을 앱에서 수집하려면 앱은 해당 샘플을 건강 데이터베이스에 추가해야 합니다
iPhone 및 iPad를 초기화하면 데이터 소스에서 수집해야 할 타입에 현재 활동에 대해 수집하려는 모든 가능한 샘플 타입이 포함됩니다 예를 들어, 외부 심박수 센서가 없는 경우 심박수가 시스템에서 생성되지 않을 수 있을 때도 심박수가 포함됩니다
데이터 소스는 시스템에서 생성되거나 앱에 의해 저장된 모든 샘플과 수집할 타입을 관찰하고 이를 실시간 빌더로 전달합니다
시스템이 실제로 생성하는 데이터를 확인하려면 UI의 지표를 업데이트할 때 사용하는 workoutBuilder 대리자를 활용할 수 있죠
앱에서 수집할 기본 타입을 수정하려면 데이터 소스의 type 메서드에서 enableCollection 또는 disableCollection을 호출해 타입을 추가하거나 빼면 됩니다
예를 들어, 운동 중 식수 섭취량을 수집하려면 앱이 enableCollection을 호출하고 운동이 진행되는 동안 측정값을 샘플로 건강 데이터베이스에 추가하며 데이터 소스는 해당 샘플을 실시간 빌더로 자동 전달하죠 이제 세션에서 지표를 가져오는 방법을 알아봤으니 운동이 저장된 후 운동 지표를 읽는 방법을 간단히 살펴보겠습니다
먼저 운동 객체의 통계 기능을 사용하여 요약을 표시합니다
운동 기간 동안 지표를 차트로 표시하려면 원하는 간격으로 통계 수집 쿼리를 사용해야 합니다
앱에 세분화된 데이터가 필요한 경우 운동과 연관된 양 샘플의 개수가 1보다 클 수 있다는 점에 유의하세요 이는 샘플이 더 세분화된 데이터를 포함함을 뜻하며 이 샘플에 접근하려면 대신 HKQuantitySeriesSampleQuery를 사용해야 합니다
Apple Watch에서 과거 운동을 확인할 때 해당하는 점이죠 iOS에 저장된 운동에도 동일하게 적용됩니다 지금까지 앱이 포그라운드에 있을 때 지표를 수집하고 운동이 저장된 후 지표를 읽는 방법을 설명했습니다 중요한 차이점은 또 있습니다 Apple Watch와 달리 iPhone은 운동 실행 시 잠길 가능성이 매우 큽니다 개인정보 보호 때문에 잠겨 있으면 건강 데이터에 접근할 수 없지만 걱정하지 마세요 운동 세션이 처음 시작될 때 시스템은 기기가 잠겨 있더라도 앱에서 운동 데이터에 접근할 수 있음을 표시합니다 이때 앱에서 중요한 지표를 포함하는 실시간 현황을 잠금 화면에 표시하면 좋습니다 사용자가 기기의 잠금을 해제하지 않고도 실시간 업데이트를 잠금 화면에서 볼 수 있죠 그런데 개인정보 보호 관련 알림이 있다고 말씀드렸죠? 기기가 잠겨 있을 때 데이터에 접근할 수 없거나 심박수 데이터를 사용 못해 지표를 보여주지 않는 경우 UI를 업데이트하여 지표를 생략하고 운동 시간만 표시하는 것이 좋습니다
하지만 잠금 화면에서 할 수 있는 작업은 더 있습니다 이제 잠금 화면에서 Siri가 다시 지원됩니다 이제 휴대폰의 잠금을 해제하지 않고도 운동을 시작, 일시 중지, 재개 및 취소할 수 있습니다 휴대폰의 잠금이 해제되면 HealthKit이 운동을 저장하고 HealthStore를 통해 운동을 앱에 제공합니다 잠금 화면에서 작동하는 Siri 인텐트를 앱에 추가하는 방법을 설명하겠습니다
IntentHandler를 먼저 정의해야 합니다 잠금 화면에서 작동하려면 앱 내부에서 처리되어야 합니다 다음으로, 지원할 인텐트를 정의합니다 쉽게 읽도록 줄을 바꾸어 각 인텐트를 구분했습니다 들어오는 인텐트를 처리합니다 StartWorkoutIntent를 받았죠 먼저 실행 중인 운동이 있는지 확인하고 있다면 failure를 반환합니다 다음으로는 활동 타입과 위치가 필요합니다 이 예제에서는 간단히 실외 달리기로 설정하겠습니다 그런 다음 success response를 반환합니다
인텐트가 정의되었으므로 이를 처리하기 위해 앱 대리자를 생성해야 합니다 그런 다음 앱 내에서 대리자를 정의합니다
이제 앱은 잠금 화면에서 Siri 운동 인텐트를 지원하죠
Siri 인텐트와 실시간 현황을 추가하면 기기의 잠금 상태와 관계없이 사용자가 앱의 핵심 기능을 유용하게 활용할 수 있습니다 이 두 기술을 자세히 알아보려면 WWDC24 ‘앱 인텐트로 사용자에게 앱의 핵심 기능 제공하기’ 및 WWDC23 ‘ActivityKit 알아보기’ 세션을 시청하세요 세션 내 지표를 수집하는 방법 및 실시간 현황에서 Siri 인텐트를 사용하는 방법을 알아봤으니 이제 충돌 복구로 넘어가죠 충돌 복구는 Watch에서 잠깐 지원된 기능입니다 세 가지 핵심 사항을 알려 드릴게요 충돌이 발생하면 시스템이 앱을 자동으로 재실행하며 빌더의 운동 세션은 이전 상태로 복원됩니다 그러나 실시간 데이터 소스는 다시 설정해야 하죠 iPhone 및 iPad에서 세션 내 운동 복구를 위해 새 장면 대리자를 추가했습니다
Siri 인텐트를 위해 생성한 앱 대리자를 사용하여 충돌 복구 처리를 위한 장면 대리자를 추가할 수 있죠 먼저 App Delegate 정의 후 options 매개변수에 shouldHandleActiveWorkoutRecovery가 있는지 확인합니다 그 후 HealthStore에서 복원된 workoutSession을 가져오고 recoveredSession을 WorkoutManager에 전달합니다 WorkoutManager에서 운동을 계속 진행할 수 있습니다 그리고 dataSource만 다시 만들면 됩니다
마지막으로 운동의 몇 가지 모범 사례를 설명하겠습니다 Watch 앱이 있다면 Watch에서 운동을 시작해야 사용 가능한 모든 지표를 얻을 수 있습니다 HealthStore에서 startWatchApp을 호출하세요 운동이 시작되면 운동을 iPhone으로 미러링하세요 WWDC23 ‘다중 기기 운동 앱 구축하기’ 세션에서 미러링하는 방법을 확인하세요
앱은 필요한 데이터 유형에 대한 권한만 요청해야 합니다 앱의 목적과 관련 없는 데이터 타입에 대한 권한을 요청하다가 사용자가 우려를 표하는 일이 없도록 하세요 마지막으로 항상 Workout Builder API를 사용하여 운동을 만들고 저장하세요 이렇게 해야 활동 링이 적절히 업데이트됩니다
iPhone 및 iPad에서 HealthKit으로 운동을 추적하는 방법이었습니다 이 업데이트로 두 기기는 강력한 API를 갖추어 충돌 후 복구하고 화면이 잠겼을 때도 운동을 표시하고 관리하는 기능을 갖췄죠 다음 단계를 알려 드리며 세션을 마칠게요 이 세션에 첨부된 데모 앱을 다운로드하세요 오늘 사용한 모든 코드가 있어 완전히 작동하는 앱으로 시작하기에 좋습니다 iPhone 또는 iPad용 앱이 이미 있다면 Workout Builder API로 업그레이드하세요 유용할 것이라 생각합니다 Apple Watch용 앱이 이미 있다면 이 API의 멀티 플랫폼 지원 덕분에 Apple Watch가 없어도 앱을 선보일 수 있죠 Apple의 여러 기능은 개발자가 의견을 공유해 주신 덕분에 만들 수 있었습니다 의견을 계속 공유해 주세요 전 세계인의 건강을 지켜주는 유용한 앱을 빌드하시는 데 필요한 기능을 계속 지원해 드리고 싶습니다 시청해 주셔서 감사합니다
-
-
1:30 - Set up workout session
// Set up workout session // Create workout configuration let configuration = HKWorkoutConfiguration() configuration.activityType = .running configuration.locationType = .outdoor // Create workout session let session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration) session.delegate = self // Get associated workout builder and add data source let builder = session.associatedWorkoutBuilder() builder.delegate = self builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
-
1:54 - Starting the session
// Prepare and start session session.prepare() // Start and display count down // Start session and builder collection once count down finishes session.startActivity(with: startDate) try await builder.beginCollection(at: startDate)
-
2:14 - Handling Metrics
// Handling collected metrics func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) { for type in collectedTypes { guard let quantityType = type as? HKQuantityType else { return } let statistics = workoutBuilder.statistics(for: quantityType) // Update the published values updateForStatistics(statistics) } }
-
2:28 - Ending workout
// Stopping the workout session session.stopActivity(with: .now) // Session transitions to stopped then call end func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { guard change.newState == .stopped, let builder else { return } try await builder.endCollection(at: change.date) let finishedWorkout = try await builder.finishWorkout() session.end() }
-
7:17 - Set up Siri Intent
// Create an INExtension within your main app // Define an intent handler public class IntentHandler: INExtension { } // Define the intents to support extension IntentHandler: INStartWorkoutIntentHandling extension IntentHandler: INPauseWorkoutIntentHandling extension IntentHandler: INResumeWorkoutIntentHandling extension IntentHandler: INEndWorkoutIntentHandling
-
7:32 - Handle the Siri intent
// Handle the intent public func handle(intent: INStartWorkoutIntent) async -> INStartWorkoutIntentResponse { let state = await WorkoutManager.shared.state switch state { case .running, .paused, .prepared, .stopped: return INStartWorkoutIntentResponse(code: .failureOngoingWorkout, userActivity: nil) default: break; } Task { await MainActor.run { // Handle the intents activity type and location WorkoutManager.shared.setWorkoutConfiguration(activityType: .running, location: .outdoor) } } return INStartWorkoutIntentResponse(code: .success, userActivity: nil) }
-
7:52 - App Delegate
// Implement an app delegate // Create app delegate class WorkoutsOniOSSampleAppDelegate: NSObject, UIApplicationDelegate { let handler = IntentHandler() func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? { return handler } } // Add app delegate to app struct WorkoutsOniOSSampleApp: App { @UIApplicationDelegateAdaptor(WorkoutsOniOSSampleAppDelegate.self) var appDelegate }
-
9:09 - Set up crash recovery
// App Delegate func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { if options.shouldHandleActiveWorkoutRecovery { let store = HKHealthStore() store.recoverActiveWorkoutSession(completion: { (workoutSession, error) in // Handle error Task { await WorkoutManager.shared.recoverWorkout(recoveredSession: workoutSession) } }) } let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) configuration.delegateClass = WorkoutsOniOSSampleAppSceneDelegate.self return configuration }
-
9:25 - Recover the workout session
// Recover the workout for the session func recoverWorkout(recoveredSession: HKWorkoutSession) { session = recoveredSession builder = recoveredSession.associatedWorkoutBuilder() session?.delegate = self builder?.delegate = self workoutConfiguration = recoveredSession.workoutConfiguration let dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: workoutConfiguration) builder?.dataSource = dataSource }
-
-
- 0:00 - 서론
HealthKit 프레임워크를 사용하면 건강 및 피트니스 앱이 사용자 건강 데이터의 중앙화된 암호화된 데이터베이스에 액세스할 수 있습니다. 플랫폼의 운동 API를 사용하면 사람들이 운동을 추적하고, 건강 지표에 액세스하며, 회복에 대한 통찰을 얻는 데 사용할 수 있는 포괄적인 운동 앱 및 iPhone을 만들 수 있습니다. 달리기 운동 세션의 기본을 배워 운동하는 동안 측정 항목에 접근하고, 충돌을 처리하며, 운동 팁과 모범 사례를 제공합니다.
- 0:56 - 운동 세션 실행하기
동일한 코드를 약간만 조정하여 iPhone, iPad, Apple Watch에서 사용할 수 있습니다. 활동을 추적하려면 운동 구성을 만들고 유형을 설정한 다음 세션을 시작합니다. 운동 빌더는 운동하는 동안 건강 지표를 수집합니다.
- 2:50 - 세션 지표 확인하기
Apple Watch를 활용하여 운동 시, 기기의 내장 센서는 원활한 경험을 제공합니다. 이와 대조적으로 iPhone 또는 iPad에서 심박수 데이터를 수집하려면 기기에 센서가 없어 외부 심박수 센서와 페어링해야 합니다. iPhone 및 iPad는 다양한 운동 측정 항목을 수집할 수 있지만, 시스템은 앱에서 특별히 요청한 샘플과 다른 샘플을 생성할 수 있습니다. 운동하는 동안 수집된 기본 데이터 유형을 수정할 수 있습니다. 누군가 운동을 저장한 후에는 시간 경과에 따른 요약 통계나 차트 지표에 액세스하여 표시할 수 있습니다. iPhone 운동은 과거 iPhone 운동과 유사하게 특정 지표에 대해 더 세부적인 데이터를 제공할 수 있습니다. iPhone은 일반적으로 운동하는 동안 잠깁니다. 개인정보 보호상의 이유로, 기기가 잠겨 있는 동안에는 일반적으로 건강 데이터에 액세스할 수 없습니다. 하지만 시스템은 잠겨 있을 때에도 앱에 대한 운동 데이터 액세스를 허용하라는 메시지를 표시할 수 있습니다. 그러면 잠금 화면에 실시간 현황을 표시하여 사용자가 휴대폰 잠금을 해제하지 않고도 중요한 지표를 볼 수 있습니다. Siri 지원이 잠금 화면까지 확장되어 사용자가 핸즈프리로 운동을 시작, 일시 정지, 재개 또는 취소할 수 있습니다. Siri 인텐트를 앱에 통합하면 이 기능을 활성화할 수 있습니다.
- 8:35 - 충돌 후 복구하기
Apple Watch에서 충돌 복구를 실행하면 앱이 자동으로 다시 시작되고 운동 세션이 복원됩니다. iPhone 및 iPad의 경우, 새로운 장면 대리인가 충돌 후 운동 세션의 복구를 처리합니다. 이러한 앱 대리자를 사용하면 복구 옵션을 확인하고 healthStore에서 복구된 세션을 검색할 수 있습니다.
- 9:34 - 모범 사례
HealthKit을 사용하여 운동을 효과적으로 추적하려면 Apple Watch 앱에서 시작한 운동을 iPhone으로 미러링하고, 필요한 데이터 승인만 요청하며, 운동 빌더 API를 활용하세요. 이러한 권장 사항을 사용하면 정확한 활동 링 업데이트를 보장하고 사용자 경험을 향상할 수 있습니다. 또한 새로운 API로 업그레이드하고, 데모 앱을 다운로드하며, iPhone 및 iPad에 운동 지원을 추가하여 시장 도달 범위 확대를 고려하세요.