In the Home Screen of my app, I have a Capsule() that contains the user's steps. This data is obtained via HealthKit. The data is correct however when it changes in the health app, it should change in my app but this is not happening. How do I get 'steps' variable to listen to HealthKit, there must be an error in my code.
Here is the code for my Home view:
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
}
}
Here is the code for the 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()))!
}
}
Here is the code for Step:
import Foundation
struct Step: Identifiable {
let id = UUID()
let count: Int
let date: Date
}
Here is the code for the .onLoad method that is used on the Home view:
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))
}
}
Any ideas?
I think I know what's missing here.
In your class HealthStore
, inside the method func calculateSteps(completion: @escaping (HKStatisticsCollection?)-> Void)
, you have set the initialResultsHandler
property of your HKStatisticsCollectionQuery
. However, you've not set the statisticsUpdateHandler
property.
To continue to listen to updates from HealthKit, after initialResultsHandler
completes execution, you need to also set statisticsUpdateHandler
.
As-per the statisticsUpdateHandler
documentation, in-case statisticsUpdateHandler
is nil, which is the case above, the HKStatisticsCollectionQuery
will:
"automatically stop as soon as it has finished calculating the initial results"
which seems to be in-line with what you are observing.
Full disclosure, I've never worked with HealthKit, so please pardon me if this doesn't help. I've gone through the HealthKit documentation after looking at your code and writing here what jumps out at me.
If it does help you though, kindly consider marking it as the answer.