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

使用ObservedObject与HealthKit时视图不更新的问题排查

问题描述

应用首页通过HealthKit获取用户步数并展示在Capsule组件中,初始数据显示正常,但当健康应用内的步数数据发生变化时,应用内的步数数据无法同步更新,需要实现对HealthKit步数变化的监听机制。

现有代码

Home视图代码

import SwiftUI
import HealthKit

struct Home: View {
        
    @ObservedObject var healthStore = HealthStore()
    @State private var steps: [Step] = [Step]()

    init() {
        healthStore = HealthStore()
    }
    
    private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
        
        let startDate = Date()
        let endDate = Date()
        
        statisticsCollection.enumerateStatistics(from: startDate, to: endDate) { (statistics, stop) in
            
            let count = statistics.sumQuantity()?.doubleValue(for: .count())
            let step = Step(count: Int(count ?? 0), date: statistics.startDate)
            steps.append(step)
        }
    }
    
    var body: some View {
        
        NavigationView {
        
        ScrollView {
            
            ZStack {
                Color("BackgroundColour")
                    .ignoresSafeArea()
                
                    VStack {
                        let totalSteps = steps.reduce(0) { $0 + $1.count }
                        ForEach($steps, id: \.id) { step in
                            Button(action: {
                                // Perform button action here
                                print("Step Capsule Tapped...")
                            }) {
                                HStack {
                                    Image("footsteps")

                                    Text("\(totalSteps)")
                                }
                            }
                        } // ForEach End
                    } // VStack End
            }//ZStack End
            .edgesIgnoringSafeArea(.all)
        } // ScrollView End
        .background(Color("BackgroundColour"))
        .onLoad {
            healthStore.requestAuthorization { success in
                if success {
                    healthStore.calculateSteps { statisticsCollection in
                        if let statisticsCollection = statisticsCollection {
                            // update the UI
                            updateUIFromStatistics(statisticsCollection)
                        }
                    }
                }
            }
        } // .onLoad End
        .onAppear(perform: {
            let defaults = UserDefaults.standard
            let keyString: String? = defaults.string(forKey: "key") ?? ""
            print("User's Key:\(keyString ?? "")")
        }) // .onAppear End
        } // NavigationView End
    }
}

HealthStore代码

import Foundation
import HealthKit
import SwiftUI
import Combine


class HealthStore: ObservableObject {
    
    @Published var healthStore: HKHealthStore?
    @Published var query: HKStatisticsCollectionQuery?
    
    init() {
        if HKHealthStore.isHealthDataAvailable() {
            healthStore = HKHealthStore()
            
        }
    }

    func calculateSteps(completion: @escaping (HKStatisticsCollection?)-> Void) {
        
        let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
        let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())
        let anchorDate = Date.mondayAt12AM()
        let daily = DateComponents(day: 1)
        let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
        let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates:
                                    [.init(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered), predicate]
                                )
        query = HKStatisticsCollectionQuery(
           quantityType: stepType,
           quantitySamplePredicate: compoundPredicate,
           options: .cumulativeSum,
           anchorDate: anchorDate,
           intervalComponents: daily)

        query!.initialResultsHandler = { query, statisticsCollection, error in
            completion(statisticsCollection)
            
        }
        if let healthStore = healthStore, let query = self.query {
            healthStore.execute(query)
        }
    }
    func requestAuthorization(completion: @escaping (Bool) -> Void) {
        
        let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
        
        guard let healthStore = self.healthStore else { return completion(false) }
        
        healthStore.requestAuthorization(toShare: [], read: [stepType]) { (success, error) in
            completion(success)
        }
    }
}

extension Date {
    static func mondayAt12AM() -> Date {
        return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
    }
}

Step模型代码

import Foundation

struct Step: Identifiable {
    let id = UUID()
    let count: Int
    let date: Date
}

.onLoad修饰符代码

import SwiftUI

struct ViewDidLoadModifier: ViewModifier {

    @State private var didLoad = false
    private let action: (() -> Void)?

    init(perform action: (() -> Void)? = nil) {
        self.action = action
    }

    func body(content: Content) -> some View {
        content.onAppear {
            if didLoad == false {
                didLoad = true
                action?()
            }
        }
    }
}

extension View {

    func onLoad(perform action: (() -> Void)? = nil) -> some View {
        modifier(ViewDidLoadModifier(perform: action))
    }

}
解决方案

问题根源

  • 现有代码仅在应用初始化时调用一次calculateSteps获取步数数据,没有监听HealthKit数据变化的机制
  • HKStatisticsCollectionQuery默认只返回初始查询结果,不会自动响应后续的健康数据更新

修复步骤

1. 重构HealthStore,添加实时监听逻辑

修改HealthStore,让它持有并发布步数数据,同时为HKStatisticsCollectionQuery添加更新回调,实现数据变化监听:

import Foundation
import HealthKit
import SwiftUI

class HealthStore: ObservableObject {
    private let healthStore: HKHealthStore?
    private var query: HKStatisticsCollectionQuery?
    @Published var dailySteps: [Step] = [] // 用@Published发布数据,让View自动监听更新

    init() {
        if HKHealthStore.isHealthDataAvailable() {
            healthStore = HKHealthStore()
        } else {
            healthStore = nil
        }
    }

    func startObservingSteps() {
        guard let healthStore = healthStore else { return }
        
        let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
        let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
        let anchorDate = Date.mondayAt12AM()
        let daily = DateComponents(day: 1)
        
        // 过滤用户手动输入的数据
        let userEnteredPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
        let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
        let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [userEnteredPredicate, datePredicate])

        query = HKStatisticsCollectionQuery(
            quantityType: stepType,
            quantitySamplePredicate: compoundPredicate,
            options: .cumulativeSum,
            anchorDate: anchorDate,
            intervalComponents: daily
        )

        // 处理初始查询结果
        query!.initialResultsHandler = { [weak self] _, statisticsCollection, error in
            guard let self = self, let statisticsCollection = statisticsCollection else { return }
            self.updateDailySteps(from: statisticsCollection)
        }

        // 监听数据变化:当HealthKit步数更新时触发
        query!.statisticsUpdateHandler = { [weak self] _, statistics, statisticsCollection, error in
            guard let self = self, let statisticsCollection = statisticsCollection else { return }
            self.updateDailySteps(from: statisticsCollection)
        }

        healthStore.execute(query!)
    }

    // 将统计数据转换为Step数组
    private func updateDailySteps(from statisticsCollection: HKStatisticsCollection) {
        var newSteps: [Step] = []
        let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
        
        statisticsCollection.enumerateStatistics(from: startDate, to: Date()) { statistics, _ in
            let count = statistics.sumQuantity()?.doubleValue(for: .count()) ?? 0
            newSteps.append(Step(count: Int(count), date: statistics.startDate))
        }
        
        // 回到主线程更新UI数据
        DispatchQueue.main.async {
            self.dailySteps = newSteps
        }
    }

    func requestAuthorization(completion: @escaping (Bool) -> Void) {
        guard let healthStore = healthStore else { return completion(false) }
        let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
        
        healthStore.requestAuthorization(toShare: [], read: [stepType]) { success, _ in
            completion(success)
        }
    }

    // 销毁时停止查询,避免内存泄漏
    deinit {
        if let query = query, let healthStore = healthStore {
            healthStore.stop(query)
        }
    }
}

extension Date {
    static func mondayAt12AM() -> Date {
        return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
    }
}

2. 简化Home视图,直接监听HealthStore数据

移除Home视图中冗余的@State steps,直接使用HealthStore发布的dailySteps,并调整初始化逻辑:

import SwiftUI
import HealthKit

struct Home: View {
    @ObservedObject var healthStore = HealthStore()

    var body: some View {
        NavigationView {
            ScrollView {
                ZStack {
                    Color("BackgroundColour")
                        .ignoresSafeArea()
                    
                    VStack {
                        let totalSteps = healthStore.dailySteps.reduce(0) { $0 + $1.count }
                        Button(action: {
                            print("Step Capsule Tapped...")
                        }) {
                            HStack {
                                Image("footsteps")
                                Text("\(totalSteps)")
                            }
                        }
                    }
                }
                .edgesIgnoringSafeArea(.all)
            }
            .background(Color("BackgroundColour"))
            .onLoad {
                healthStore.requestAuthorization { success in
                    if success {
                        healthStore.startObservingSteps()
                    }
                }
            }
            .onAppear {
                let defaults = UserDefaults.standard
                let keyString = defaults.string(forKey: "key") ?? ""
                print("User's Key:\(keyString)")
            }
        }
    }
}

3. 关键优化说明

  • 实时监听核心:HKStatisticsCollectionQuery的statisticsUpdateHandler会在HealthKit数据变化时触发,我们在这里重新计算步数并更新数据
  • 响应式UI:HealthStore用@Published修饰dailySteps,Home视图通过@ObservedObject自动监听数据变化,无需手动刷新UI
  • 内存管理:在HealthStore的deinit方法中停止查询,避免内存泄漏
  • UI简化:移除原视图中多余的ForEach遍历,直接计算总步数展示即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 04:15:39