Search code examples
iosswifthealthkit

How to make sure this code runs in the correct order?


I have this code to get data from the health store. I am looping through information for each of the different sample types that I want to get from the health store in the fetchSampleData function. I am expecting the function to loop through and get all of the different kinds of samples and then use the completion to send that data back to the getData function. I then want to use that data in my code.

func getData() {
    // Data types and units for health kit queries
    let sampleInfo = [
        ("heartRate", HKObjectType.quantityType(
                forIdentifier: .heartRate), "count/min"),
        ("heartRateVariabilitySDNN", HKObjectType.quantityType(
                forIdentifier: .heartRateVariabilitySDNN), "s"),
        ("stepCount", HKObjectType.quantityType(
                forIdentifier: .stepCount), "count"),
    ]
        
    self.fetchSampleData(
        sampleInfo: sampleInfo
    ) { samplesDict in

          let sampleCount = samplesDict.count

          print("Sample count: \(sampleCount)")
          print(samplesDict)

          if (sampleCount > 0) {
              // code to do stuff with samplesDict ...
          }
        
    }
}

public func fetchSampleData(
                sampleInfo: [(String, HKQuantityType?, String)],
                completion: @escaping ( _ samples: 
                    Dictionary<String, Dictionary<Date, Double>>) -> Void
    ) {
    
    let numberOfDataTypes = sampleInfo.count
    var resultsDict: [String: [Date: Double]] = [:]
    
    for i in 0..<numberOfDataTypes {
        let dataTypeName = sampleInfo[i].0
        let sampleType = sampleInfo[i].1!
        let unitString = sampleInfo[i].2
        var dataTypeDictionary = [Date:Double]()

        // Predicate for specifying start and end dates for the query
        let predicate = HKQuery
            .predicateForSamples(
                withStart: Date.distantPast,
                end: Date.now,
                options: .strictEndDate)
        
        // Set sorting by date.
        let sortDescriptor = NSSortDescriptor(
            key: HKSampleSortIdentifierStartDate,
            ascending: false)
        
        // Create the query
        let query = HKSampleQuery(
            sampleType: sampleType,
            predicate: predicate,
            limit: Int(HKObjectQueryNoLimit),
            sortDescriptors: [sortDescriptor]) { (_, results, error) in
                
                guard error == nil else {
                    print("Error: \(error!.localizedDescription)")
                    return
                }

                print("~~- \(results?.count) \(dataTypeName) samples returned")
                for sample in results ?? [] {
                    let data = sample as! HKQuantitySample
                    let unit = HKUnit(from: unitString)
                    let sampleVal = data.quantity.doubleValue(for: unit)
                    let dateStart = data.startDate
                    
                    dataTypeDictionary[dateStart] = sampleVal
                }

                resultsDict[dataTypeName] = dataTypeDictionary
            }
        
        healthStore.execute(query)
    }

    completion(resultsDict)
}

But when I run this code I am getting this for an output:

Sample count: 0

[:]

~~- Optional(103) heartRateVariabilitySDNN samples returned

~~- Optional(846) stepCount samples returned

~~- Optional(3127) activeEnergyBurned samples returned

~~- Optional(4470) heartRate samples returned

~~- Optional(2913) basalEnergyBurned samples returned

So the code that i was assuming would run after the fetchSampleData function was done seems to be running before the data is even returned. Is there a way that I could tell the program to only run the code involving the returned samplesDict data if the fetchSampleData has finished getting that data?


Solution

  • Your fetchSampleData function loops through numberOfDataTypes, firing off an async query to the health store each time through the loop. Then, before any of those async calls can complete, you call your function's completion handler. If you want to wait until all the queries complete you should set up a GCD DispatchGroup to synchronize all of those calls.

    I just created a question and answer explaining DispatchGroups and providing a simple example using them.

    Appling that approach to your code might look like the below (see all the MARK: New code marks in the code for the changes.)

        public func fetchSampleData(
            sampleInfo: [(String, HKQuantityType?, String)],
            completion: @escaping ( _ samples:
                                        Dictionary<String, Dictionary<Date, Double>>) -> Void
        ) {
            
            let numberOfDataTypes = sampleInfo.count
            var resultsDict: [String: [Date: Double]] = [:]
            
            // -------------------------------
            // MARK: New code
            let dispatchGroup = DispatchGroup()
            
            // Create a DispatchWorkItem to call our completion handler once all our tasks have finished.
            let workItem = DispatchWorkItem() {
                completion(resultsDict)
            }
            // -------------------------------
            
            for i in 0..<numberOfDataTypes {
                let dataTypeName = sampleInfo[i].0
                let sampleType = sampleInfo[i].1!
                let unitString = sampleInfo[i].2
                var dataTypeDictionary = [Date:Double]()
                
                // Predicate for specifying start and end dates for the query
                let predicate = HKQuery
                    .predicateForSamples(
                        withStart: Date.distantPast,
                        end: Date.now,
                        options: .strictEndDate)
                
                // Set sorting by date.
                let sortDescriptor = NSSortDescriptor(
                    key: HKSampleSortIdentifierStartDate,
                    ascending: false)
                
                // Create the query
                // MARK: New code
                dispatchGroup.enter() // Tell the dispatch group we have added another async task
                let query = HKSampleQuery(
                    sampleType: sampleType,
                    predicate: predicate,
                    limit: Int(HKObjectQueryNoLimit),
                    sortDescriptors: [sortDescriptor]) { (_, results, error) in
                        
                        guard error == nil else {
                            print("Error: \(error!.localizedDescription)")
                            return
                        }
                        
                        print("~~- \(results?.count) \(dataTypeName) samples returned")
                        for sample in results ?? [] {
                            let data = sample as! HKQuantitySample
                            let unit = HKUnit(from: unitString)
                            let sampleVal = data.quantity.doubleValue(for: unit)
                            let dateStart = data.startDate
                            
                            dataTypeDictionary[dateStart] = sampleVal
                        }
                        
                        resultsDict[dataTypeName] = dataTypeDictionary
                        // MARK: New code
                        dispatchGroup.leave() // tell the dispatch group this task has been completed
                        
                    }
                
                healthStore.execute(query)
            }
            dispatchGroup.notify(queue: DispatchQueue.main, work: workItem)  // MARK: New code
        }
    }