使用 HealthKit 卡在完成处理程序/调度组

Stuck at completion handlers / dispatch groups with HealthKit

我在 class:

的初始化中调用了一个函数
doLongTask(forPast: 6)

这是我和调度组一起做的游乐场:

//MAKE THE STRUCT
struct Calorie: Identifiable {

    private static var idSequence = sequence(first: 1, next: {[=11=] + 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)

它应该做的是进行 HKStatisticsQuery 调用(稍后会有 3 次调用)并将结果写回我的数组。

但是,它会在函数完成之前写入数组,因此不会返回正确的值。我尝试使用调度组,但我卡住了。

getEnergyTemp() 的完成处理程序中的 print(value) 在测试循环完成后打印出正确的值。

我哪里错了?我以为我已经理解了这个原理,但我就是做不到。

主要问题是您在错误的地方调用了 leave。所以,而不是:

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()
    }
}

请记住 getTempEnergy 闭包是异步调用的(即稍后调用)。您需要将 leave 调用和结果附加 移到 闭包中:

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()
        }
    }
}

请注意,这会使旧变量 _activeEnergyBurned 过时,您现在可以将其删除。


无关,但是:

  1. 注意我已经删除了 group 引用作为 queue.async 的参数。当您手动调用 enterleave 时,asyncgroup 参数现在是多余的。

    在处理本身是异步的调用(这里就是这种情况)时,您通常使用 enter/leave 模式,或者使用 group 参数来 async 当分派的代码是同步的。但不是两者。

  2. 请注意 getTempEnergy 中的所有路径都必须调用完成处理程序(否则您的 DispatchGroup 可能永远无法解析)。因此,在 getTempEnergy 中的那个 guard 语句中也必须调用 completion

    这就引出了为 completion 闭包提供什么价值的问题。一种方法是使 Double 参数可选,并使 return nil 出现错误。或者,更可靠的方法是使用 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)
    }
    

    然后

    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()
            }
        }
    }
    

    您的自定义错误所在位置:

    enum HKProjectError: Error {
        case noValue
        case invalidType
    }
    
  3. 你关心你的数组内容是否有序吗?请注意,这些异步方法可能不会按照您启动它们的相同顺序完成。如果顺序很重要,您可能希望将结果保存在字典中,然后通过数字 day 作为索引检索值。所以,也许用

    替换你的数组
    var calorieResults = [Int: Calorie]()
    

    然后,在保存结果时:

    calorieResults[day] = Calorie(...)
    

    然后,完成后,像这样检索结果:

    group.notify(queue: queue) {
        print("All tasks done")
        for day in 0...days {
            print(day, calorieResults[day] ?? "No data found")
        }
        print("---")
    }
    

    这还有一个好处,就是如果一天或多天失败了,您就会知道是哪几天失败了。例如,如果您只有和数组,并且您获得了上周 7 天中的 5 天的数据,您将不知道丢失了哪些天。但是通过为您的模型使用字典,您现在知道哪些天有数据,哪些天没有。

  4. 避免使用 sleep。这会阻塞当前线程。如果您想定期检查发生了什么,请改用计时器。