使用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
相关产品推荐
相关产品推荐

