I have a function that I call in the init of my class:
doLongTask(forPast: 6)
This is the playground I made with the dispatch groups:
//MAKE THE STRUCT
struct Calorie: Identifiable {
private static var idSequence = sequence(first: 1, next: {$0 + 1})
var id: Int
var day: Date
var activeCal: CGFloat
var restingCal: CGFloat
var dietaryCal: CGFloat
init?(id: Int, day: Date, activeCal:CGFloat, restingCal:CGFloat, dietaryCal:CGFloat) {
guard let id = Calorie.idSequence.next() else { return nil}
self.id = id
self.day = day
self.activeCal = activeCal
self.restingCal = restingCal
self.dietaryCal = dietaryCal
}
}
//CREATE HEALTHSTORE
let healthStore = HKHealthStore()
//MAKE TEST ARRAY
var testCalorieArray = [Calorie]()
func doLongTask(forPast days: Int) {
print("Enter the function!")
print("---")
func getTempEnergy (for type:HKQuantityType!, unit u:HKUnit!, start fromDate:Date, end endDate:Date, completion: @escaping (Double) -> Void) {
let countQuantityType = type
let predicate = HKQuery.predicateForSamples(withStart: fromDate, end: endDate, options: .strictStartDate)
let query = HKStatisticsQuery(quantityType: countQuantityType!, quantitySamplePredicate: predicate, options: .cumulativeSum) { (_, result, error) in
var resultCount = 0.0
guard let result = result else {
return
}
if let sum = result.sumQuantity() {
resultCount = sum.doubleValue(for: u)
}
DispatchQueue.main.async {
print(resultCount)
completion(resultCount)
}
}
healthStore.execute(query)
}
let queue = DispatchQueue(label: "com.WILDFANGmedia.queues.serial")
let group = DispatchGroup()
let now = Calendar.current.startOfDay(for: Date())
//Initialize to test values to see if they get overwritten
var _activeEnergyBurned:CGFloat = 99.9
var _restingEnergyBurned:CGFloat = 99.9
var _dietaryEnergyConsumed:CGFloat = 99.9
//EACH DAY
for day in 0...days {
group.enter()
queue.async(group: group) {
// Start und Enddatum
let fromDate = Calendar.current.date(byAdding: .day, value: -day-1, to: now)!
let endDate = Calendar.current.date(byAdding: .day, value: -day, to: now)!
getTempEnergy(for: HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned), unit: HKUnit.kilocalorie(), start: fromDate, end: endDate, completion: { (activeEnergyBurned) in
print(activeEnergyBurned)
_activeEnergyBurned = CGFloat(activeEnergyBurned)
})
print("End Datum: \(endDate)")
print("Active Cal: \(_activeEnergyBurned)")
print("Day \(day) done")
print("---")
testCalorieArray.append(Calorie(id: 1, day: endDate, activeCal: CGFloat(_activeEnergyBurned), restingCal: CGFloat(_restingEnergyBurned), dietaryCal: CGFloat(_dietaryEnergyConsumed))!)
group.leave()
}
}
//SHOW WHAT'S IN THE ARRAY
group.notify(queue: queue) {
print("All tasks done")
print(testCalorieArray)
print("---")
}
//AFTER LOOP, GO ON WITH BUSINESS
print("Continue execution immediately")
}
doLongTask(forPast: 6)
print("AFTER THE FUNCTION")
//TEST LOOP TO RUN LONGER
for i in 1...7 {
sleep(arc4random() % 2)
print("Outter Row Task \(i) done")
}
print(testCalorieArray)
What it should do, is, to make the HKStatisticsQuery
call (there will be 3 calls later) and write the result back into my array.
However, it writes to the array BEFORE the function is finished, hence not returning the correct values. I tried with the dispatch groups but I am stuck.
The print(value)
in the completion handler of getEnergyTemp()
is printed out in the very end after the test loop is finished and it prints out the correct values.
Where am I going wrong? I thought I had understood this principle but I just can't make it work.
The main issue is that you’re calling leave
in the wrong place. So, instead of:
for day in 0...days {
group.enter()
queue.async(group: group) {
...
getTempEnergy(for: HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned), unit: HKUnit.kilocalorie(), start: fromDate, end: endDate, completion: { (activeEnergyBurned) in
print(activeEnergyBurned)
_activeEnergyBurned = CGFloat(activeEnergyBurned)
})
...
testCalorieArray.append(Calorie(id: 1, day: endDate, activeCal: CGFloat(_activeEnergyBurned), restingCal: CGFloat(_restingEnergyBurned), dietaryCal: CGFloat(_dietaryEnergyConsumed))!)
group.leave()
}
}
Remember that the getTempEnergy
closure is called asynchronously (i.e. later). You need to move the leave
call and the appending of the results inside that closure:
for day in 0...days {
group.enter()
queue.async {
...
getTempEnergy(for: .quantityType(forIdentifier: .activeEnergyBurned), unit: .kilocalorie(), start: fromDate, end: endDate) { (activeEnergyBurned) in
testCalorieArray.append(Calorie(id: 1, day: endDate, activeCal: CGFloat(activeEnergyBurned), restingCal: CGFloat(_restingEnergyBurned), dietaryCal: CGFloat(_dietaryEnergyConsumed))!)
group.leave()
}
}
}
Note that this renders that old variable, _activeEnergyBurned
, obsolete and you can now remove it.
Unrelated, but:
Note I have eliminated group
reference as parameter to queue.async
. When you’re manually calling enter
and leave
, the group
parameter to async
is now redundant.
You generally either use enter
/leave
pattern when dealing with calls that are, themselves, asynchronous (which is the case here) or you use group
parameter to async
when the dispatched code is synchronous. But not both.
Be very careful that all paths within getTempEnergy
must call the completion handler (otherwise your DispatchGroup
may never be resolved). So, in that guard
statement inside getTempEnergy
must also call completion
.
That begs the question of what value to supply to that completion
closure. One approach is to make the Double
parameter optional and return nil
upon error. Or, the more robust approach is to use Result
type:
func getTempEnergy (for type: HKQuantityType?, unit u: HKUnit, start fromDate: Date, end endDate: Date, completion: @escaping (Result<Double, Error>) -> Void) {
guard let countQuantityType = type else {
completion(.failure(HKProjectError.invalidType))
return
}
let predicate = HKQuery.predicateForSamples(withStart: fromDate, end: endDate, options: .strictStartDate)
let query = HKStatisticsQuery(quantityType: countQuantityType, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
DispatchQueue.main.async {
guard error == nil else {
completion(.failure(error!))
return
}
guard let resultCount = result?.sumQuantity()?.doubleValue(for: u) else {
completion(.failure(HKProjectError.noValue))
return
}
completion(.success(resultCount))
}
}
healthStore.execute(query)
}
and then
for day in 0...days {
group.enter()
queue.async {
let fromDate = Calendar.current.date(byAdding: .day, value: -day-1, to: now)!
let endDate = Calendar.current.date(byAdding: .day, value: -day, to: now)!
getTempEnergy(for: .quantityType(forIdentifier: .activeEnergyBurned), unit: .kilocalorie(), start: fromDate, end: endDate) { result in
switch result {
case .failure(let error):
print(error)
case .success(let activeEnergyBurned):
testCalorieArray.append(Calorie(id: 1, day: endDate, activeCal: CGFloat(activeEnergyBurned), restingCal: CGFloat(_restingEnergyBurned), dietaryCal: CGFloat(_dietaryEnergyConsumed))!)
}
group.leave()
}
}
}
Where your custom errors are:
enum HKProjectError: Error {
case noValue
case invalidType
}
Do you care about whether the contents of your array is in order or not? Be aware that these asynchronous methods might not complete in the same order that you initiated them. If order matters, you might want to save the results in a dictionary and then retrieve the values by the numerical day
as an index. So, perhaps replace your array with
var calorieResults = [Int: Calorie]()
And then, when saving the results:
calorieResults[day] = Calorie(...)
And then, when you’re all done, retrieve the results like so:
group.notify(queue: queue) {
print("All tasks done")
for day in 0...days {
print(day, calorieResults[day] ?? "No data found")
}
print("---")
}
This also has the virtue that if one or more days failed, you know for which ones. If you only had and array and you, for example, got data for 5 of the 7 days in the last week, you wouldn’t know which days are missing. But by using the dictionary for your model, you now know for which days you have data and for which you don’t.
Avoid using sleep
. That blocks the current thread. Instead use a timer if you want to periodically check what’s going on.