Застрял в обработчиках завершения / групп отправки с HealthKit - PullRequest
1 голос
/ 25 января 2020

У меня есть функция, которую я вызываю в начале моего класса:

doLongTask(forPast: 6)

Это игровая площадка, которую я создал с группами рассылки:

//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)

Что она должна делать , это, чтобы сделать вызов HKStatisticsQuery (будет 3 вызова позже) и записать результат обратно в мой массив.

Однако, он записывает в массив ДО завершения функции, следовательно, не возвращает правильные значения. Я пытался с группами рассылки, но я застрял.

print(value) в обработчике завершения getEnergyTemp() распечатывается в самом конце после завершения теста l oop и распечатывает правильные значения.

Куда я иду? Я думал, что понял этот принцип, но просто не могу заставить его работать.

1 Ответ

2 голосов
/ 25 января 2020

Основная проблема в том, что вы звоните 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. Когда вы вручную вызываете enter и leave, параметр group для async теперь избыточен.

    Обычно вы используете либо шаблон enter / leave при работе с вызовами, которые сами являются асинхронными (что имеет место в данном случае) или вы используете параметр group для async, когда отправленный код является синхронным. Но не оба.

  2. Будьте очень осторожны, чтобы все пути в getTempEnergy должны вызывать обработчик завершения (в противном случае ваш DispatchGroup может никогда не быть разрешен). Таким образом, в этом guard операторе внутри getTempEnergy также должен вызываться completion.

    Возникает вопрос, какое значение придать этому закрытию completion. Один из подходов состоит в том, чтобы сделать параметр Double необязательным и вернуть nil в случае ошибки. Или, более надежный подход заключается в использовании Result типа:

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

    Это также имеет то преимущество, что, если один или несколько дней провалились, вы знаете, для каких из них. Если бы у вас был и массив, и вы, например, получили данные за 5 из 7 дней на прошлой неделе, вы бы не знали, какие дни пропущены. Но используя словарь для вашей модели, вы теперь знаете, за какие дни у вас есть данные, а за какие нет.

  4. Избегайте использования sleep. Это блокирует текущий поток. Вместо этого используйте таймер, если вы хотите периодически проверять, что происходит.

...