HKObserverQuery updateHandler is getting fired twice in a row

Hi,

I'm trying to implement HealthKit Background delivery for my app and I'm getting a strange behavior where the HKObserverQuery updateHandler is getting fired twice. I'm running Xcode 13.4 & iOS 15.5.

2022-06-10 10:05:58.725006+0200 Cori[79737:8949689] [HealthKit] Run Background Delivery Handler

2022-06-10 10:05:58.726283+0200 Cori[79737:8949689] [HealthKit] Run Background Delivery Handler

2022-06-10 10:05:58.736475+0200 Cori[79737:8949700] [HealthKit] New data: [200 mg/dL 4680E3F5-034A-4AED-843C-9066532AD459 "Salud" (15.5), "iPhone14,2" (15.5)metadata: {

2022-06-10 10:05:58.736632+0200 Cori[79737:8949700] [HealthKit] 1 new HKQuantityTypeIdentifierBloodGlucose samples

2022-06-10 10:05:58.736926+0200 Cori[79737:8949689] [persistence] HealthKit: Start import to Core Data

2022-06-10 10:05:58.737116+0200 Cori[79737:8949700] [HealthKit] New data: [200 mg/dL 4680E3F5-034A-4AED-843C-9066532AD459 "Salud" (15.5), "iPhone14,2" (15.5)metadata: {

2022-06-10 10:05:58.737985+0200 Cori[79737:8949689] [persistence] HealthKit: 1 samples of type HKQuantityTypeIdentifierBloodGlucose not from this app!

2022-06-10 10:05:58.753583+0200 Cori[79737:8949689] [persistence] HealthKit: Start batch insert request.

2022-06-10 10:05:58.753630+0200 Cori[79737:8949700] [HealthKit] 1 new HKQuantityTypeIdentifierBloodGlucose samples

2022-06-10 10:05:58.753873+0200 Cori[79737:8949689] [persistence] HealthKit: Start import to Core Data

2022-06-10 10:05:58.754019+0200 Cori[79737:8949689] [persistence] HealthKit: 1 samples of type HKQuantityTypeIdentifierBloodGlucose not from this app!

2022-06-10 10:05:58.754076+0200 Cori[79737:8949689] [persistence] HealthKit: Start batch insert request.

2022-06-10 10:05:58.759208+0200 Cori[79737:8949701] [persistence] HealthKit: Successfully imported data.

2022-06-10 10:05:58.759669+0200 Cori[79737:8949701] [persistence] HealthKit: Successfully imported data.

This is my code:

App Delegate

extension AppDelegate {

    public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if settings.syncHealthKit {
HealthKit.shared.setUpBackgroundDelivery()
        }
        return true

    }

HealthKit Class

class HealthKit: ObservableObject {
    private let logger = Logger(subsystem: "com.ChubbyApps.Diabetes", category: "HealthKit")

    private var dataController: DataController = .shared
    private var ajustes: AjustesModel = .shared

    public let isAvailable = HKHealthStore.isHealthDataAvailable()
    private lazy var isAuthorized = false

    // MARK: - Properties
    private var anchor: HKQueryAnchor? {
        get {
            guard let data = NSUbiquitousKeyValueStore.default.object(forKey: Keys.HKAnchor) as? Data else {
                return nil
            }
            do {
                return try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
            } catch {
                logger.error("Unable to unarchive \(data): \(error.localizedDescription)")
                return nil
            }
        }

        set(newAnchor) {
            guard let newAnchor = newAnchor else {
                return
            }
            do {
                let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: true)
                NSUbiquitousKeyValueStore.default.set(data, forKey: Keys.HKAnchor)
            } catch {
                logger.error("Unable to archive \(newAnchor): \(error.localizedDescription)")
            }
        }
    }

    // MARK: - Initializers
    static let shared = HealthKit()
    
// MARK: - Public Methods
    public func requestAuthorization() async -> Bool {
        guard isAvailable else { return false }
        do {
            try await HKStore.requestAuthorization(toShare: types, read: types)
            self.isAuthorized = true
            return true
        } catch let error {
            self.logger.error("An error occurred while requesting HealthKit Authorization: \(error.localizedDescription)")
            return false
        }
    }
    // MARK: - Background

    func setUpBackgroundDelivery() {
        let query: HKObserverQuery = HKObserverQuery(sampleType: glucose, predicate: nil, updateHandler: self.backgroundDeliveryHandler)
        HKStore.execute(query)
        HKStore.enableBackgroundDelivery(for: glucose, frequency: .immediate) { success, error in
            if success {
                self.logger.debug("Enabled background delivery of stepcount changes")
            } else {
                if let theError = error {
                    self.logger.debug("Failed to enable background delivery of stepcount changes. Error: \(theError.localizedDescription)")
                }
            }
        }
    }

    func backgroundDeliveryHandler(query: HKObserverQuery!, completionHandler: HKObserverQueryCompletionHandler!, error: Error!) {
        self.logger.debug("Run Background Delivery Handler")
        self.anchoredQueryFor(types) 
        completionHandler()
    }

    private func anchoredQueryFor(_ tipos: Set<HKSampleType>) {
        var queryDescriptors = [HKQueryDescriptor]()
        for type in tipos {
            queryDescriptors.append(HKQueryDescriptor(sampleType: type, predicate: nil))
        }

        let anchoredQuery = HKAnchoredObjectQuery(
            queryDescriptors: queryDescriptors,
            anchor: anchor,
            limit: HKObjectQueryNoLimit) { _, newSamples, _, newAnchor, error in
                if let error = error {
                    self.logger.error("HKAnchoredObjectQuery error: \(error.localizedDescription)")
                    return
                }
                self.anchor = newAnchor
                guard let samples = newSamples as? [HKQuantitySample] else { return }
                guard !samples.isEmpty else { return }
                self.logger.debug("New data: \(samples.debugDescription)")
                for type in types {
                    let filteredSamples = samples.filter({ $0.quantityType == type })
                    if  !filteredSamples.isEmpty {
                        self.logger.debug("\(filteredSamples.count) new \(type.debugDescription) samples")
                        Task {
                            do {
                                try await self.dataController.importHealthKitSample(filteredSamples, type: type)
                            } catch {
                                self.logger.error("importHealthKitSample error: \(error.localizedDescription)")
                            }
                        }
                    }
                }
            }
        HKStore.execute(anchoredQuery)
    }

I also tried with:

    func setUpBackgroundDelivery() async {
        var queryDescriptors = [HKQueryDescriptor]()
        for type in types {
            queryDescriptors.append(HKQueryDescriptor(sampleType: type, predicate: nil))
        }
        let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { query, sampleTypesWithNewData, completionHandler, error in
            if let error = error {
                self.logger.error("HKObserverQuery error: \(error.localizedDescription)")
                completionHandler()
                return
            }
            for type in sampleTypesWithNewData! {
                self.logger.debug("New \(type.debugDescription) data")
                self.anchoredQueryFor(sampleTypesWithNewData!)
            }
            completionHandler()
        }

        HKStore.execute(observerQuery)

        for type in types {
            do {
                try await HKStore.enableBackgroundDelivery(for: type, frequency: .hourly)
            } catch {
                self.logger.error("setUpBackgroundDeliveryFor \(type) - error: \(error.localizedDescription)")
            }
        }
    }

And the update handler gets called twice:

2022-06-10 10:54:06.464318+0200 Cori[81627:9010600] [HealthKit] New HKQuantityTypeIdentifierBloodGlucose data

2022-06-10 10:54:06.467039+0200 Cori[81627:9010600] [HealthKit] New HKQuantityTypeIdentifierBloodGlucose data

2022-06-10 10:54:06.472524+0200 Cori[81627:9010601] [HealthKit] New data: [222 mg/dL 6AA78B15-4EB5-4F80-823E-DFBC43AB3C5A "Salud" (15.5), "iPhone14,2" (15.5)metadata: {

2022-06-10 10:54:06.472742+0200 Cori[81627:9010601] [HealthKit] 1 new HKQuantityTypeIdentifierBloodGlucose samples

2022-06-10 10:54:06.472889+0200 Cori[81627:9010591] [persistence] HealthKit: Start import to Core Data

2022-06-10 10:54:06.473032+0200 Cori[81627:9010591] [persistence] HealthKit: 1 samples of type HKQuantityTypeIdentifierBloodGlucose not from this app!

2022-06-10 10:54:06.473103+0200 Cori[81627:9010591] [persistence] HealthKit: Start batch insert request.

2022-06-10 10:54:06.473356+0200 Cori[81627:9010601] [HealthKit] New data: [222 mg/dL 6AA78B15-4EB5-4F80-823E-DFBC43AB3C5A "Salud" (15.5), "iPhone14,2" (15.5)metadata: {

2022-06-10 10:54:06.475530+0200 Cori[81627:9010591] [persistence] HealthKit: Successfully imported data.

2022-06-10 10:54:06.493688+0200 Cori[81627:9010601] [HealthKit] 1 new HKQuantityTypeIdentifierBloodGlucose samples

2022-06-10 10:54:06.493899+0200 Cori[81627:9010591] [persistence] HealthKit: Start import to Core Data

2022-06-10 10:54:06.494013+0200 Cori[81627:9010591] [persistence] HealthKit: 1 samples of type HKQuantityTypeIdentifierBloodGlucose not from this app!

2022-06-10 10:54:06.494057+0200 Cori[81627:9010591] [persistence] HealthKit: Start batch insert request.

2022-06-10 10:54:06.495381+0200 Cori[81627:9010601] [persistence] HealthKit: Successfully imported data.

Did you ever find a solution to this? I'm also seeing duplicate samples returned sometimes, but can't figure out a way to reproduce it reliably.

Hi @eschos24!

I still have the problem but found a little workaround by building a semaphore using Swift Actor's. So the import function only fires once every 10 seconds.

actor Semaphore {
    static let shared = Semaphore()
    
    var isImporting = false

    func run(sleeping: UInt32 = 5, function: @escaping () async -> Void) async {

        if !isImporting {

            isImporting = true

            await function()

            sleep(sleeping)

            isImporting = false

        }
    }
}

This is it in action

private func setUpBackgroundDelivery() async {
        var queryDescriptors = [HKQueryDescriptor]()

        for type in types {
            queryDescriptors.append(HKQueryDescriptor(sampleType: type, predicate: nil))
        }

        let observerQuery = HKObserverQuery(queryDescriptors: queryDescriptors) { query, sampleTypesWithNewData, completionHandler, error in
            if let error = error {
                completionHandler()
                return
            }

            Task {
                await self.semaphore.run(sleeping: 10) {
                    await self.checkForNewWorkouts()
                }
                completionHandler()
            }
        }

        HKStore.execute(observerQuery)

        do {
            try await HKStore.enableBackgroundDelivery(for: workout, frequency: .hourly)
        } catch {

        }
    }
HKObserverQuery updateHandler is getting fired twice in a row
 
 
Q