Search code examples
swifthealthkit

Swift HealthKit HKStatisticsCollectionQuery statisticsUpdateHandler not always called


I have a test project where I get the total number of falls for a user for each day over the course of the week. The initialResultsHandler works perfectly every time, however the statisticsUpdateHandler doesn't always fire off. If you start the app, then go to the health app and insert falls manually, switch back to the test app you should see the total for today update. In reality this works for about the first 3-6 times. After that the statisticsUpdateHandler doesn't get called anymore.

What's also odd is that if you delete data and then go back to the test app, or add data from a time earlier than now, the statisticsUpdateHandler gets called. This leads me to think that it has something to do with the statisticsUpdateHandler end date.

Apples documentation is pretty clear however I’m afraid they might be leaving something out.

If this property is set to nil, the statistics collection query will automatically stop as soon as it has finished calculating the initial results. If this property is not nil, the query behaves similarly to the observer query. It continues to run, monitoring the HealthKit store. If any new, matching samples are saved to the store—or if any of the existing matching samples are deleted from the store—the query executes the update handler on a background queue.

Is there any reason that statisticsUpdateHandler might not be called? I have included a test project below.

struct Falls: Identifiable{
    let id = UUID()
    let date: Date
    let value: Int
    
    var formattedDate: String{
        let formatter = DateFormatter()
        formatter.setLocalizedDateFormatFromTemplate("MM/dd/yyyy")
        return formatter.string(from: date)
    }
}
struct ContentView: View {
    @StateObject var manager = HealthKitManager()
    
    var body: some View {
        NavigationView{
            List{
                Text("Updates: \(manager.updates)")
                ForEach(manager.falls){ falls in
                    HStack{
                        Text(falls.value.description)
                        Text(falls.formattedDate)
                    }
                }
            }
            .overlay(
                ProgressView()
                    .scaleEffect(1.5)
                    .opacity(manager.isLoading ? 1 : 0)
            )
            .navigationTitle("Falls")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
class HealthKitManager: ObservableObject{
    let healthStore = HKHealthStore()
    let fallType   = HKQuantityType.quantityType(forIdentifier: .numberOfTimesFallen)!
    @Published var isLoading = false
    @Published var falls = [Falls]()
    @Published var updates = 0

    init() {
        let healthKitTypesToRead: Set<HKSampleType> = [fallType]

        healthStore.requestAuthorization(toShare: nil, read: healthKitTypesToRead) { (success, error) in
            if let error = error{
                print("Error: \(error)")
            } else if success{
                self.startQuery()
            }
        }
    }
    
    func startQuery(){
        let now = Date()
        let cal = Calendar.current
        let sevenDaysAgo = cal.date(byAdding: .day, value: -7, to: now)!
        let startDate = cal.startOfDay(for: sevenDaysAgo)
        let predicate = HKQuery.predicateForSamples(withStart: startDate, end: now, options: [.strictStartDate, .strictEndDate])
        
        var interval = DateComponents()
        interval.day = 1
        
        // start from midnight
        let anchorDate = cal.startOfDay(for: now)
       
        let query = HKStatisticsCollectionQuery(
            quantityType: fallType,
            quantitySamplePredicate: predicate,
            options: .cumulativeSum,
            anchorDate: anchorDate,
            intervalComponents: interval
        )
        
        query.initialResultsHandler = { query, collection, error in
            guard let collection = collection else {
                print("No collection")
                DispatchQueue.main.async{
                    self.isLoading = false
                }
                return
            }
            
            collection.enumerateStatistics(from: startDate, to: Date()){ (result, stop) in
                guard let sumQuantity = result.sumQuantity() else {
                    return
                }
                
                let totalFallsForADay = Int(sumQuantity.doubleValue(for: .count()))
                let falls = Falls(date: result.startDate, value: totalFallsForADay)
                print(falls.value, falls.formattedDate)
                
                DispatchQueue.main.async{
                    self.falls.insert(falls, at: 0)
                }
            }
            
            print("initialResultsHandler done")
            DispatchQueue.main.async{
                self.isLoading = false
            }
        }
        
        
        query.statisticsUpdateHandler = { query, statistics, collection, error in
            print("In statisticsUpdateHandler...")
            guard let collection = collection else {
                print("No collection")
                DispatchQueue.main.async{
                    self.isLoading = false
                }
                return
            }
            
            DispatchQueue.main.async{
                self.isLoading = true
                self.updates += 1
                self.falls.removeAll(keepingCapacity: true)
            }
            
            collection.enumerateStatistics(from: startDate, to: Date()){ (result, stop) in
                guard let sumQuantity = result.sumQuantity() else {
                    return
                }
                
                let totalFallsForADay = Int(sumQuantity.doubleValue(for: .count()))
                let falls = Falls(date: result.startDate, value: totalFallsForADay)
                print(falls.value, falls.formattedDate)
                print("\n\n")

                DispatchQueue.main.async{
                    self.falls.insert(falls, at: 0)
                }
            }
            
            print("statisticsUpdateHandler done")
            DispatchQueue.main.async{
                self.isLoading = false
            }
        }
        
        
        isLoading = true
        healthStore.execute(query)
    }
}

Solution

  • I was so focused on the statisticsUpdateHandler and the start and end time that I didn't pay attention to the query itself. It turns out that the predicate was the issue. By giving it an end date, it was never looking for samples outside the the initial predicate end date.

    Changing the predicate to this solved the issue:

    let predicate = HKQuery.predicateForSamples(withStart: startDate, end: nil, options: [.strictStartDate])