❤️ HealthKit 완전정복
건강 앱의 데이터를 읽고 쓰세요. 걸음수, 심박수, 수면, 운동 등 모든 건강 데이터에 접근!
✨ HealthKit이란?
HealthKit은 Apple 건강 앱의 중앙 저장소입니다. iPhone, Apple Watch의 모든 건강 데이터를 읽고 쓸 수 있습니다.
🔐 권한 요청
Authorization.swift
import HealthKit class HealthKitManager { let healthStore = HKHealthStore() // HealthKit 사용 가능 여부 func isHealthKitAvailable() -> Bool { return HKHealthStore.isHealthDataAvailable() } // 권한 요청 func requestAuthorization() async throws { // 읽을 데이터 타입 let typesToRead: Set<HKObjectType> = [ HKObjectType.quantityType(forIdentifier: .stepCount)!, HKObjectType.quantityType(forIdentifier: .heartRate)!, HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!, HKObjectType.workoutType() ] // 쓸 데이터 타입 let typesToWrite: Set<HKSampleType> = [ HKObjectType.quantityType(forIdentifier: .stepCount)!, HKObjectType.workoutType() ] try await healthStore.requestAuthorization( toShare: typesToWrite, read: typesToRead ) } }
📊 걸음수 읽기
StepCount.swift
import HealthKit func fetchTodaySteps() async throws -> Double { let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)! // 오늘 00:00부터 현재까지 let now = Date() let startOfDay = Calendar.current.startOfDay(for: now) let predicate = HKQuery.predicateForSamples( withStart: startOfDay, end: now, options: .strictStartDate ) // 통계 쿼리 let query = HKStatisticsQuery( quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum ) { _, result, error in guard let result = result, let sum = result.sumQuantity() else { return } let steps = sum.doubleValue(for: HKUnit.count()) print("오늘 걸음수: \(steps)") } healthStore.execute(query) // async/await 변환 return try await withCheckedThrowingContinuation { continuation in let query = HKStatisticsQuery( quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum ) { _, result, error in if let error = error { continuation.resume(throwing: error) } else if let sum = result?.sumQuantity() { let steps = sum.doubleValue(for: HKUnit.count()) continuation.resume(returning: steps) } else { continuation.resume(returning: 0) } } healthStore.execute(query) } }
💓 심박수 읽기
HeartRate.swift
func fetchLatestHeartRate() async throws -> Double? { let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)! // 최신 1개만 let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) return try await withCheckedThrowingContinuation { continuation in let query = HKSampleQuery( sampleType: heartRateType, predicate: nil, limit: 1, sortDescriptors: [sortDescriptor] ) { _, samples, error in if let error = error { continuation.resume(throwing: error) } else if let sample = samples?.first as? HKQuantitySample { let bpm = sample.quantity.doubleValue(for: HKUnit(from: "count/min")) continuation.resume(returning: bpm) } else { continuation.resume(returning: nil) } } healthStore.execute(query) } } // 사용 예시 Task { if let heartRate = try await fetchLatestHeartRate() { print("현재 심박수: \(heartRate) BPM") } }
✍️ 데이터 쓰기
WriteData.swift
// 걸음수 추가 func saveSteps(_ steps: Double, date: Date) async throws { let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)! let quantity = HKQuantity(unit: HKUnit.count(), doubleValue: steps) let sample = HKQuantitySample( type: stepType, quantity: quantity, start: date, end: date ) try await healthStore.save(sample) } // 체중 기록 func saveWeight(_ weight: Double) async throws { let weightType = HKQuantityType.quantityType(forIdentifier: .bodyMass)! let quantity = HKQuantity(unit: HKUnit.gramUnit(with: .kilo), doubleValue: weight) let now = Date() let sample = HKQuantitySample( type: weightType, quantity: quantity, start: now, end: now ) try await healthStore.save(sample) }
🏃 운동 기록 (Workout)
Workout.swift
import HealthKit func saveRunningWorkout( distance: Double, duration: TimeInterval, calories: Double ) async throws { let startDate = Date().addingTimeInterval(-duration) let endDate = Date() // 운동 생성 let workout = HKWorkout( activityType: .running, start: startDate, end: endDate, duration: duration, totalEnergyBurned: HKQuantity( unit: HKUnit.kilocalorie(), doubleValue: calories ), totalDistance: HKQuantity( unit: HKUnit.meter(), doubleValue: distance ), metadata: [ HKMetadataKeyIndoorWorkout: false ] ) try await healthStore.save(workout) } // 운동 조회 func fetchWorkouts() async throws -> [HKWorkout] { let workoutType = HKObjectType.workoutType() let sortDescriptor = NSSortDescriptor( key: HKSampleSortIdentifierStartDate, ascending: false ) return try await withCheckedThrowingContinuation { continuation in let query = HKSampleQuery( sampleType: workoutType, predicate: nil, limit: 20, sortDescriptors: [sortDescriptor] ) { _, samples, error in if let error = error { continuation.resume(throwing: error) } else { let workouts = samples as? [HKWorkout] ?? [] continuation.resume(returning: workouts) } } healthStore.execute(query) } }
📈 일별 통계 (Statistics Collection)
DailyStats.swift
// 최근 7일 걸음수 func fetchWeeklySteps() async throws -> [(Date, Double)] { let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)! let now = Date() let startDate = Calendar.current.date(byAdding: .day, value: -7, to: now)! // 일별 집계 var interval = DateComponents() interval.day = 1 let predicate = HKQuery.predicateForSamples( withStart: startDate, end: now, options: .strictStartDate ) return try await withCheckedThrowingContinuation { continuation in let query = HKStatisticsCollectionQuery( quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: startDate, intervalComponents: interval ) query.initialResultsHandler = { _, results, error in if let error = error { continuation.resume(throwing: error) return } var data: [(Date, Double)] = [] results?.enumerateStatistics(from: startDate, to: now) { statistics, _ in let steps = statistics.sumQuantity()?.doubleValue(for: HKUnit.count()) ?? 0 data.append((statistics.startDate, steps)) } continuation.resume(returning: data) } healthStore.execute(query) } }
🔔 실시간 업데이트 (Observer Query)
ObserverQuery.swift
class HealthObserver { let healthStore = HKHealthStore() var query: HKObserverQuery? func startObserving() { let stepType = HKObjectType.quantityType(forIdentifier: .stepCount)! query = HKObserverQuery(sampleType: stepType, predicate: nil) { _, completionHandler, error in if let error = error { print("에러: \(error)") return } // 걸음수 변경 감지 print("걸음수가 업데이트되었습니다!") Task { let steps = try await fetchTodaySteps() print("현재 걸음수: \(steps)") } // 백그라운드 작업 완료 알림 completionHandler() } healthStore.execute(query!) // 백그라운드 전달 활성화 healthStore.enableBackgroundDelivery(for: stepType, frequency: .immediate) { success, error in print("백그라운드 전달: \(success)") } } func stopObserving() { if let query = query { healthStore.stop(query) } } }
📱 SwiftUI 통합
HealthView.swift
import SwiftUI @Observable class HealthViewModel { var steps: Double = 0 var heartRate: Double? var isAuthorized = false let manager = HealthKitManager() func requestPermission() async { do { try await manager.requestAuthorization() isAuthorized = true await loadData() } catch { print("권한 요청 실패: \(error)") } } func loadData() async { do { steps = try await fetchTodaySteps() heartRate = try await fetchLatestHeartRate() } catch { print("데이터 로드 실패: \(error)") } } } struct HealthDashboard: View { var viewModel = HealthViewModel() var body: some View { VStack(spacing: 20) { if viewModel.isAuthorized { VStack { Text("🚶 걸음수") .font(.headline) Text("\(Int(viewModel.steps))") .font(.system(size: 48, weight: .bold)) } if let heartRate = viewModel.heartRate { VStack { Text("❤️ 심박수") .font(.headline) Text("\(Int(heartRate)) BPM") .font(.system(size: 36, weight: .bold)) } } } else { Button("건강 데이터 접근 권한 요청") { Task { await viewModel.requestPermission() } } .buttonStyle(.borderedProminent) } } .task { if viewModel.isAuthorized { await viewModel.loadData() } } } }
⚙️ Info.plist 설정
Info.plist
<!-- 필수: 사용 목적 설명 --> <key>NSHealthShareUsageDescription</key> <string>건강 데이터를 읽어 운동 기록을 보여줍니다</string> <key>NSHealthUpdateUsageDescription</key> <string>운동 기록을 건강 앱에 저장합니다</string> <!-- Xcode Capabilities에서 "HealthKit" 추가 필수 -->
💡 HIG 체크리스트
✅ 필요한 데이터 타입만 요청
✅ 사용 목적 명확히 설명 (Info.plist)
✅ 권한 거부 시 앱이 정상 동작
✅ 건강 데이터는 절대 외부로 전송 금지
✅ 백그라운드 업데이트 신중히 사용