You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用HealthKit时SwiftUI图表数据重复重绘问题求助

问题:HealthKit静息心率数据读取导致图表重绘异常与数据重复

问题描述

从HealthKit读取静息心率数据时,使用带更新处理器的长期查询在后台新增数据,但每次关闭再打开应用,图表会异常重绘,下方列表也产生重复数据。尝试用.onDisappear修饰符停止查询,但仅在强制重启应用后才生效。疑问:这是否是SwiftUI Chart的问题?

相关代码

ViewModel部分

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

View部分

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

异常截图

静息心率图表异常


问题分析与解决方案

这不是SwiftUI Chart的问题,核心原因是数据处理逻辑和查询生命周期管理不当:

核心问题点

  1. 数据重复添加:每次调用calculateRestingHRData时,没有先清空restingHR数组,旧数据保留的同时新数据又被追加;且statisticsUpdateHandler里重新枚举了全部统计数据,导致所有数据被重复添加一遍。
  2. 查询未正确停止:.onDisappear未生效,大概率是因为没有正确调用healthStore.stop(_:)终止查询,且ViewModel作为EnvironmentObject可能未被销毁,查询持续运行并向数组中添加数据。

修复方案

  1. 初始化查询前清空数据
    在calculateRestingHRData方法开头添加:

    DispatchQueue.main.async {
        self.restingHR.removeAll()
    }
    

    避免旧数据残留导致重复。

  2. 优化更新处理器逻辑
    statisticsUpdateHandler中的statistics参数就是新增的单条统计数据,无需遍历整个collection,直接处理这条数据即可:

    restingHRquery!.statisticsUpdateHandler = {
        restingQuery, statistics, statisticsCollection, error in
        
        // 错误处理保留
        if let error = error as? HKError {
            switch error.code {
            case .errorHealthDataUnavailable, .errorNoData:
                return
            default:
                return
            }
        }
        
        // 仅处理新增的单条统计数据
        guard let statistics = statistics,
              let restHRquantity = statistics.averageQuantity() else { return }
        
        let hrdate = statistics.startDate
        let hrUnit = HKUnit(from: "count/min")
        let restHRvalue = restHRquantity.doubleValue(for: hrUnit)
        let restHR = RestingHeartRate(restingValue: Int(restHRvalue), date: hrdate)
        
        DispatchQueue.main.async {
            // 可选:先检查是否已存在该日期的数据,避免重复
            if !self.restingHR.contains(where: { $0.date == hrdate }) {
                self.restingHR.append(restHR)
            }
        }
    }
    
  3. 正确实现查询停止逻辑
    在ViewModel中添加停止查询的方法:

    func stopRestingHRQuery() {
        if let query = restingHRquery {
            healthStore?.stop(query)
            restingHRquery = nil
        }
    }
    

    然后在View的.onDisappear中调用:

    struct OneWeekRestHRChartView: View {
        @EnvironmentObject var healthStoreVM: HealthStoreViewModel
        
        var body: some View {
            // ... 原有View代码 ...
        }
        .onDisappear {
            healthStoreVM.stopRestingHRQuery()
        }
    }
    
  4. 确保数据唯一性
    给RestingHeartRate实现Identifiable协议,或者用日期作为唯一标识去重,避免同一日期的数据被多次添加。


内容的提问来源于stack exchange,提问作者Kevin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 17:54:59