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