I am having issues with my chart data when reading the data from HealthKit. I'm using a long running query with an update handler to add new data/values in the background. However, my chart data seems to be redrawing whenever I close and open the app which makes the line go crazy. My list below also gets additional repeated values. I tried fixing this issue by stopping the query using a .onDisappear modifier but the .onDisappear wouldn't update the values unless I force closed the app and opened it again.
//Properties within my view model for resting HR.
var restingHRquery: HKStatisticsCollectionQuery?
@Published var restingHR: [RestingHeartRate] = [RestingHeartRate]()
func calculateRestingHRData() {
let restingHRpredicate = HKQuery.predicateForSamples(withStart: oneWeekAgo, end: nil, options: .strictStartDate)
restingHRquery = HKStatisticsCollectionQuery(quantityType: restingHeartRateType,
quantitySamplePredicate: restingHRpredicate,
options: .discreteAverage,
anchorDate: anchorDate,
intervalComponents: daily)
restingHRquery!.initialResultsHandler = {
restingQuery, statisticsCollection, error in
//Handle errors here
if let error = error as? HKError {
switch (error.code) {
case .errorHealthDataUnavailable:
return
case .errorNoData:
return
default:
return
}
}
guard let statisticsCollection = statisticsCollection else { return}
//Calculating resting HR
statisticsCollection.enumerateStatistics(from: self.startDate, to: self.date) { statistics, stop in
if let restHRquantity = statistics.averageQuantity() {
let hrdate = statistics.startDate
//HR Units
let hrUnit = HKUnit(from: "count/min")
let restHRvalue = restHRquantity.doubleValue(for: hrUnit)
let restHR = RestingHeartRate(restingValue: Int(restHRvalue), date: hrdate)
DispatchQueue.main.async {
self.restingHR.append(restHR)
}
}
}
}
restingHRquery!.statisticsUpdateHandler = {
restingQuery, statistics, statisticsCollection, error in
//Handle errors here
if let error = error as? HKError {
switch (error.code) {
case .errorHealthDataUnavailable:
return
case .errorNoData:
return
default:
return
}
}
guard let statisticsCollection = statisticsCollection else { return}
//Calculating resting HR
statisticsCollection.enumerateStatistics(from: self.startDate, to: self.date) { statistics, stop in
if let restHRquantity = statistics.averageQuantity() {
let hrdate = statistics.startDate
//HR Units
let hrUnit = HKUnit(from: "count/min")
let restHRvalue = restHRquantity.doubleValue(for: hrUnit)
let restHR = RestingHeartRate(restingValue: Int(restHRvalue), date: hrdate)
DispatchQueue.main.async {
self.restingHR.append(restHR)
}
}
}
}
guard let restingHRquery = self.restingHRquery else { return }
self.healthStore?.execute(restingHRquery)
}
struct OneWeekRestHRChartView: View {
@EnvironmentObject var healthStoreVM: HealthStoreViewModel
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Average: \(healthStoreVM.averageRestHR) bpm")
.font(.headline)
Chart {
ForEach(healthStoreVM.restingHR.reversed(), id: \.date) {
restHrData in
LineMark(x: .value("day", restHrData.date, unit: .day),
y: .value("RHR", restHrData.restingValue)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(.red)
.symbol() {
Circle()
.fill(.red)
.frame(width: 15)
}
}
}
.frame(height: 200)
.chartYScale(domain: 30...80)
.chartXAxis {
AxisMarks(values: .stride(by: .day)) {
AxisGridLine()
AxisValueLabel(format: .dateTime.day().month(), centered: true)
}
}
}
.padding(.horizontal)
.navigationTitle("Resting Heart Rate")
List{
ForEach(healthStoreVM.restingHR.reversed(), id: \.date) {
restHR in
DataListView(imageText: "heart.fill",
imageColor: .red,
valueText: "\(restHR.restingValue) bpm",
date: restHR.date)
}
}
.listStyle(.inset)
}
}
Could this just be a SwiftUI chart issue?
The statisticsUpdateHandler
is not called with only the new results. It is called with an updated complete set of results.
You need to clear your existing results from restingHR
before you add the results again.
restingHRquery!.statisticsUpdateHandler = {
restingQuery, statistics, statisticsCollection, error in
//Handle errors here
if let error = error as? HKError {
switch (error.code) {
case .errorHealthDataUnavailable:
return
case .errorNoData:
return
default:
return
}
}
guard let statisticsCollection = statisticsCollection else { return}
DispatchQueue.main.async {
self.restingHR.removeAll()
//Calculating resting HR
statisticsCollection.enumerateStatistics(from: self.startDate, to: self.date) { statistics, stop in
if let restHRquantity = statistics.averageQuantity() {
let hrdate = statistics.startDate
//HR Units
let hrUnit = HKUnit(from: "count/min")
let restHRvalue = restHRquantity.doubleValue(for: hrUnit)
let restHR = RestingHeartRate(restingValue: Int(restHRvalue), date: hrdate)
self.restingHR.append(restHR)
}
}
}
}