I am currently developing an app that measures HRV to estimate stress levels.
To align the values more closely with those from Galaxy devices, I decided not to use the heartRateVariabilitySDNN value provided by HealthKit.
Instead, I extracted individual interbeat intervals (IBI) using the HKHeartBeatSeries data.
Can I obtain accurate IBI data using this method?
If not, I would like to know how I can retrieve more precise data.
Any insights or suggestions would be greatly appreciated.
Here is a sample code I tried.
@Observable class HealthKitManager: ObservableObject { let healthStore = HKHealthStore() var ibiValues: [Double] = [] var isAuthorized = false func requestAuthorization() { let types = Set([ HKSeriesType.heartbeat(), HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!, ]) healthStore.requestAuthorization(toShare: nil, read: types) { success, error in DispatchQueue.main.async { self.isAuthorized = success if success { self.fetchIBIData() } } } } func fetchIBIData() { var timePoints: [TimeInterval] = [] var absoluteStartTime: Date? let dateFormatter = DateFormatter() dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" var calendar = Calendar.current calendar.timeZone = TimeZone(identifier: "Asia/Seoul") ?? .current var components = DateComponents() components.year = 2025 components.month = 4 components.day = 3 components.hour = 15 components.minute = 52 components.second = 0 let startTime = calendar.date(from: components)! components.hour = 16 components.minute = 0 let endTime = calendar.date(from: components)! let predicate = HKQuery.predicateForSamples(withStart: startTime, end: endTime, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) let query = HKSampleQuery(sampleType: HKSeriesType.heartbeat(), predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { (_, samples, _) in if let sample = samples?.first as? HKHeartbeatSeriesSample { absoluteStartTime = sample.startDate let startDateKST = dateFormatter.string(from: sample.startDate) let endDateKST = dateFormatter.string(from: sample.endDate) print("series start(KST):\(startDateKST)\tend(KST):\(endDateKST)") let seriesQuery = HKHeartbeatSeriesQuery(heartbeatSeries: sample) { query, timeSinceSeriesStart, precededByGap, done, error in if !precededByGap { timePoints.append(timeSinceSeriesStart) } if done { for i in 1..<timePoints.count { let ibi = (timePoints[i] - timePoints[i-1]) * 1000 // Convert to milliseconds // Calculate absolute time for current beat if let startTime = absoluteStartTime { let beatTime = startTime.addingTimeInterval(timePoints[i]) let beatTimeString = dateFormatter.string(from: beatTime) print("IBI: \(String(format: "%.2f", ibi)) ms at \(beatTimeString)") } self.ibiValues.append(ibi) } } } self.healthStore.execute(seriesQuery) } else { print("No samples found for the specified time range") } } self.healthStore.execute(query) } }