Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Accuracy of IBI Values Measured by Apple Watch

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)
    }
}

Yeah, the method your code snippet demonstrates looks right. If you see any problem when going along this path, feel free to follow up here.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accuracy of IBI Values Measured by Apple Watch
 
 
Q