이 글은 누구를 위한 것인가
- 걸음수, 심박수, 수면 등 건강 데이터를 앱에 통합하려는 팀
- 운동 기록 앱을 HealthKit과 연동하려는 개발자
- Apple Watch 운동 세션을 구현하려는 팀
들어가며
HealthKit은 사용자의 건강 데이터에 접근하는 중앙 저장소다. 걸음수, 심박수, 수면 분석, 칼로리 등 다양한 데이터를 읽고 쓸 수 있다. 사용자 동의가 필수며, 요청하는 데이터 타입을 최소화해야 한다.
이 글은 bluefoxdev.kr의 iOS HealthKit 피트니스 앱 가이드 를 참고하여 작성했습니다.
1. HealthKit 설계 원칙
[HealthKit 데이터 타입]
수량 타입 (HKQuantityType):
stepCount: 걸음수
heartRate: 심박수 (beats/min)
activeEnergyBurned: 활성 칼로리
distanceWalkingRunning: 이동 거리
bodyMass: 체중
범주 타입 (HKCategoryType):
sleepAnalysis: 수면 (in bed, asleep)
mindfulSession: 마음챙김
운동 (HKWorkoutType):
workout: 운동 세션
[권한 요청 원칙]
필요한 타입만 요청
읽기 + 쓰기 각각 별도 요청
거부 시 기능 비활성화 (크래시 X)
[Info.plist]
NSHealthShareUsageDescription: "읽기 사용 설명"
NSHealthUpdateUsageDescription: "쓰기 사용 설명"
2. HealthKit 구현
import HealthKit
import Combine
class HealthKitManager: ObservableObject {
private let healthStore = HKHealthStore()
@Published var todaySteps: Int = 0
@Published var heartRate: Double = 0
@Published var sleepHours: Double = 0
// 권한 요청
func requestAuthorization() async throws {
guard HKHealthStore.isHealthDataAvailable() else {
throw HealthKitError.notAvailable
}
let readTypes: Set<HKObjectType> = [
HKQuantityType(.stepCount),
HKQuantityType(.heartRate),
HKQuantityType(.activeEnergyBurned),
HKCategoryType(.sleepAnalysis),
]
let writeTypes: Set<HKSampleType> = [
HKQuantityType(.activeEnergyBurned),
HKWorkoutType.workoutType(),
]
try await healthStore.requestAuthorization(toShare: writeTypes, read: readTypes)
}
// 오늘 걸음수 조회
func fetchTodaySteps() async -> Int {
let type = HKQuantityType(.stepCount)
let calendar = Calendar.current
let startOfDay = calendar.startOfDay(for: Date())
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date())
return await withCheckedContinuation { continuation in
let query = HKStatisticsQuery(
quantityType: type,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { _, statistics, _ in
let steps = statistics?.sumQuantity()?.doubleValue(for: .count()) ?? 0
continuation.resume(returning: Int(steps))
}
healthStore.execute(query)
}
}
// 심박수 실시간 관찰 (Observer Query)
func startHeartRateObserver() {
let type = HKQuantityType(.heartRate)
let observerQuery = HKObserverQuery(sampleType: type, predicate: nil) { [weak self] _, completionHandler, error in
guard error == nil else { return }
Task {
let latest = await self?.fetchLatestHeartRate() ?? 0
await MainActor.run { self?.heartRate = latest }
}
completionHandler()
}
healthStore.execute(observerQuery)
healthStore.enableBackgroundDelivery(for: type, frequency: .immediate) { _, _ in }
}
private func fetchLatestHeartRate() async -> Double {
let type = HKQuantityType(.heartRate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
return await withCheckedContinuation { continuation in
let query = HKSampleQuery(sampleType: type, predicate: nil, limit: 1, sortDescriptors: [sortDescriptor]) { _, samples, _ in
let bpm = (samples?.first as? HKQuantitySample)?.quantity.doubleValue(for: HKUnit(from: "count/min")) ?? 0
continuation.resume(returning: bpm)
}
healthStore.execute(query)
}
}
// 운동 세션 기록
func saveWorkout(type: HKWorkoutActivityType, startDate: Date, endDate: Date, calories: Double, distance: Double) async throws {
let workout = HKWorkout(
activityType: type,
start: startDate,
end: endDate,
duration: endDate.timeIntervalSince(startDate),
totalEnergyBurned: HKQuantity(unit: .kilocalorie(), doubleValue: calories),
totalDistance: HKQuantity(unit: .meter(), doubleValue: distance),
metadata: nil
)
try await healthStore.save(workout)
}
}
enum HealthKitError: Error { case notAvailable }
마무리
HealthKit의 핵심은 사용자 동의다. 요청하는 데이터 타입을 최소화하고, 거부 시 앱이 우아하게 기능을 비활성화해야 한다. HKObserverQuery + 백그라운드 배달로 앱이 백그라운드에서도 심박수, 걸음수 변화를 실시간으로 받아 처리할 수 있다.