iosswiftswiftuihealthkit

How do I fix my chart data being displayed when using HealthKit?


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?

enter image description here


Solution

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