❤️ Mastering HealthKit
Read and write Health app data — steps, heart rate, sleep, workouts and all health data!
⭐ Difficulty: ⭐⭐⭐
⏱️ Est. Time: 2-3h
📂 App Services
✨ HealthKit is?
HealthKit is the central repository for the Apple Health app. Read and write all health data from iPhone and Apple Watch.
🔐 Permission Request
Authorization.swift
import HealthKit class HealthKitManager { let healthStore = HKHealthStore() // HealthKit available 여부 func isHealthKitAvailable() -> Bool { return HKHealthStore.isHealthDataAvailable() } // Permission Request 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 ) } }
📊 Reading Step Count
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) } }
💓 Reading Heart Rate
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) } } // Usage Example Task { if let heartRate = try await fetchLatestHeartRate() { print("현재 심박수: \(heartRate) BPM") } }
✍️ Writing Data
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 Recording
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) } }
📈 Daily 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) } }
🔔 Real-time Updates (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: \(error)") return } // 걸음수 변경 감지 print("걸음수가 Updated!") 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 Integration
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("Permission Request 실패: \(error)") } } func loadData() async { do { steps = try await fetchTodaySteps() heartRate = try await fetchLatestHeartRate() } catch { print("데이터 Load failed: \(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("건강 데이터 접근 Permission Request") { Task { await viewModel.requestPermission() } } .buttonStyle(.borderedProminent) } } .task { if viewModel.isAuthorized { await viewModel.loadData() } } } }
⚙️ Info.plist Configuration
Info.plist
<!-- 필수: 사용 목적 설명 --> <key>NSHealthShareUsageDescription</key> <string>건강 데이터를 읽어 운동 기록을 보여줍니다</string> <key>NSHealthUpdateUsageDescription</key> <string>운동 기록을 건강 앱에 saves</string> <!-- Xcode Capabilities에서 "HealthKit" 추가 필수 -->
💡 HIG Checklist
✅ 필요한 데이터 타입만 요청
✅ 사용 목적 명확히 설명 (Info.plist)
✅ 권한 거부 시 앱이 정상 동작
✅ 건강 데이터는 절대 외부로 전송 금지
✅ 백그라운드 업데이트 신중히 사용