Search code examples
swiftuihealthkit

SwiftUI - DispatchQueue.main.async in HealthKit


I want to load the following GUI in SwiftUI:

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var test = Test()
    @ObservedObject var healthStore = HealthStore()
    
    
    func callUpdate() {
        
        print(test.value)
        print(healthStore.systolicValue)
        print(healthStore.diastolicValue)
    }
    
    var body: some View {
        Text("Platzhalter")
            .padding()
            .onAppear(perform: {
            healthStore.setUpHealthStore()
                callUpdate()
            })
        
        Button("Test"){
            callUpdate()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The variables healthStore.systolicValue and healthStore.diastolicValue are called via the function callUpdate(). On the first call both variables are nil. Only when I call the function via the Test button, the correct value is output in the console.

The variables healthStore.systolicValue and healthStore.diastolicValue are calculated in the class HealthStore:

import Foundation
import HealthKit

class HealthStore: ObservableObject {

    var healthStore: HKHealthStore?
    var query: HKStatisticsQuery?
    
    public var systolicValue: HKQuantity?
    public var diastolicValue: HKQuantity?

    init() {
        if HKHealthStore.isHealthDataAvailable() {
            healthStore = HKHealthStore()
        }
    }

    func setUpHealthStore() {
        let typesToRead: Set = [
            HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!,
            HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!
        ]

        healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
            if success {
                print("requestAuthrization")
                self.calculateBloodPressureSystolic()
                self.calculateBloodPressureDiastolic()
            }
        })

    }

    func calculateBloodPressureSystolic() {
        guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
            // This should never fail when using a defined constant.
            fatalError("*** Unable to get the bloodPressure count ***")
        }
        query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
                                  quantitySamplePredicate: nil,
                                  options: .discreteAverage) {
            query, statistics, error in

            DispatchQueue.main.async{
                self.systolicValue = statistics?.averageQuantity()
            }
        }

        healthStore!.execute(query!)
    }
    
    func calculateBloodPressureDiastolic() {
        guard let bloodPressureDiastolic = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
            // This should never fail when using a defined constant.
            fatalError("*** Unable to get the bloodPressure count ***")
        }
        query = HKStatisticsQuery(quantityType: bloodPressureDiastolic,
                                  quantitySamplePredicate: nil,
                                  options: .discreteAverage) {
            query, statistics, error in

            DispatchQueue.main.async{
                self.diastolicValue = statistics?.averageQuantity()
            }
        }

        healthStore!.execute(query!)
    }
    
    
}

How do I need to modify my code to get the correct value for healthStore.systolicValue and healthStore.diastolicValue directly when I call ContentView?


Solution

  • This is the full code that I use for testing and works for me, using macos 11.4, xcode 12.5, target ios 14.5, tested on iPhone device. Let us know if this does not work for you.

    import SwiftUI
    import HealthKit
    
    @main
    struct TestErrorApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    class HealthStore: ObservableObject {
        var healthStore: HKHealthStore?
        var query: HKStatisticsQuery?
        @Published var systolicValue: HKQuantity?
        @Published var diastolicValue: HKQuantity?
        
        init() {
            if HKHealthStore.isHealthDataAvailable() {
                healthStore = HKHealthStore()
            }
        }
    
        func setUpHealthStore() {
            let typesToRead: Set = [
                HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!,
                HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!
            ]
            healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
                if success {
                    print("--> requestAuthorization")
                    self.calculateBloodPressureSystolic()
                    self.calculateBloodPressureDiastolic()
                }
            })
        }
        
        func calculateBloodPressureSystolic() {
            guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
                // This should never fail when using a defined constant.
                fatalError("*** Unable to get the bloodPressure count ***")
            }
            query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
                                      quantitySamplePredicate: nil,
                                      options: .discreteAverage) {
                query, statistics, error in
                DispatchQueue.main.async{
                  //  self.systolicValue = statistics?.averageQuantity()
                    self.systolicValue = HKQuantity(unit: HKUnit(from: ""), doubleValue: 1.2)
                    print("----> calculateBloodPressureSystolic statistics: \(statistics)")
                    print("----> calculateBloodPressureSystolic error: \(error)")
                    print("----> calculateBloodPressureSystolic: \(self.systolicValue)")
                }
            }
            healthStore!.execute(query!)
        }
        
        func calculateBloodPressureDiastolic() {
            guard let bloodPressureDiastolic = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
                // This should never fail when using a defined constant.
                fatalError("*** Unable to get the bloodPressure count ***")
            }
            query = HKStatisticsQuery(quantityType: bloodPressureDiastolic,
                                      quantitySamplePredicate: nil,
                                      options: .discreteAverage) {
                query, statistics, error in
                DispatchQueue.main.async{
                   // self.diastolicValue = statistics?.averageQuantity()
                    self.diastolicValue = HKQuantity(unit: HKUnit(from: ""), doubleValue: 3.4)
                    print("----> calculateBloodPressureDiastolic statistics: \(statistics)")
                    print("----> calculateBloodPressureDiastolic error: \(error)")
                    print("----> calculateBloodPressureDiastolic: \(self.diastolicValue)")
                }
            }
            healthStore!.execute(query!)
        }
    }
    
    struct ContentView: View {
        @ObservedObject var healthStore = HealthStore()
        var bloodPressureStandard = HKQuantity(unit: HKUnit(from: ""), doubleValue: 0.0)
        
        var body: some View {
            VStack {
                Text("systolicValue: \(healthStore.systolicValue ?? bloodPressureStandard)")
                Text("diastolicValue: \(healthStore.diastolicValue ?? bloodPressureStandard)")
            }.onAppear {
                healthStore.setUpHealthStore()
            }
        }
    }
    

    This is the output I get:

    --> requestAuthorization

    ----> calculateBloodPressureSystolic statistics: nil

    ----> calculateBloodPressureSystolic error: Optional(Error Domain=com.apple.healthkit Code=11 "No data available for the specified predicate." UserInfo={NSLocalizedDescription=No data available for the specified predicate.})

    ----> calculateBloodPressureSystolic: Optional(1.2 ())

    ----> calculateBloodPressureDiastolic statistics: nil

    ----> calculateBloodPressureDiastolic error: Optional(Error Domain=com.apple.healthkit Code=11 "No data available for the specified predicate." UserInfo={NSLocalizedDescription=No data available for the specified predicate.})

    ----> calculateBloodPressureDiastolic: Optional(3.4 ())

    And the UI shows:

    systolicValue: 1.2 ()

    diastolicValue: 3.4 ()