Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

HKAnchoredObjectQuery Stops Receiving Updates

I implemented this to receive updates for specific data types and keep the latest daily information up to date. However, for some reason, it only works for a while before stopping completely.

Background Delivery

    internal func backgroundDeliveryForReadTypes(enable: Bool, types: Set<HKQuantityType>) async {
        do {
            if enable {
                try await statusForAuthorizationRequest(toWrite: [], toRead: types)
                for type in types {
                    try await healthStore.enableBackgroundDelivery(for: type, frequency: .daily)
                }
            } else {
                for type in types {
                    try await healthStore.disableBackgroundDelivery(for: type)
                }
            }
            
        } catch {
            debugPrint("Error enabling background delivery: \(error.localizedDescription)")
        }
    }

HKQueryAnchor

    internal var walkingActivityQueryAnchor: HKQueryAnchor? {
        get {
            if let anchorData = UserDefaults.standard.data(forKey: "walkingActivityAnchor") {
                return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: anchorData)
            }
            return nil
        }
        set {
            if let newAnchor = newValue {
                let anchorData = try? NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: true)
                UserDefaults.standard.set(anchorData, forKey: "walkingActivityAnchor")
            } else {
                UserDefaults.standard.removeObject(forKey: "walkingActivityAnchor")
            }
        }
    }

HKAnchoredObjectQuery

    internal func observeWalkingActivityInBackground(
        _ start: Bool,
        toRead: Set<HKQuantityType>,
        completion: @escaping @Sendable (Result<WalkingActivityData?, Error>) -> Void
    ) {
        if start {
            guard (walkingActivityQuery == nil) else {
                return
            }
            
            let predicate = getPredicate(date: Date())
            let queryDescriptors = toRead.map {
                HKQueryDescriptor(sampleType: $0, predicate: predicate)
            }

            let handleSamples: @Sendable (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = { [weak self] _, samples, _, newAnchor, error in
                guard let self = self else { return }

                if let error = error {
                    completion(.failure(error))
                    return
                }

                guard let samples = samples, !samples.isEmpty else {
                    completion(.success(nil))
                    return
                }

                Task {
                    self.walkingActivityQueryAnchor = newAnchor
                    
                    let activity = await self.getWalkingActivity(date: Date())
                    completion(.success(activity))
                }
            }

            let query = HKAnchoredObjectQuery(
                queryDescriptors: queryDescriptors,
                anchor: walkingActivityQueryAnchor,
                limit: HKObjectQueryNoLimit,
                resultsHandler: handleSamples
            )

            query.updateHandler = handleSamples
            healthStore.execute(query)

            walkingActivityQuery = query
        } else {
            if let query = walkingActivityQuery {
                healthStore.stop(query)
                walkingActivityQuery = nil
            }
        }
    }

WalkingActivityData

    private func getWalkingActivity(date: Date) async -> WalkingActivityData {
        async let averageHeartRate = try await self.getAverageHeartRate(date: date)
        async let steps = try self.getStepCount(date: date)
        async let durationMinutes = try self.getTotalDurationInMinutes(date: date)
        async let distanceMeters = try self.getDistanceWalkingRunning(date: date, unit: .meter())
        async let activeCalories = try self.getActiveEnergyBurned(date: date)
        
        return await WalkingActivityData(
            date: date,
            steps: try? steps,
            activeCalories: try? activeCalories,
            distanceMeters: try? distanceMeters,
            durationMinutes: try? durationMinutes,
            averageHeartRate: try? averageHeartRate
        )
}

Example of getAverageHeartRate

    func getAverageHeartRate(date: Date) async throws -> Double? {
        let type = HKQuantityType(.heartRate)
        _ = try checkAuthorizationStatus(for: type)
        
        guard let heartRate = try await getDescriptor(
            date: date,
            type: type,
            options: .discreteAverage
        ).result(for: healthStore)
            .statistics(for: date)?
            .averageQuantity()?.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute()))
        else {
            return nil
        }
        
        return Double(String(format: "%.2f", heartRate)) ?? 0.0
    }

Descriptor & predicate

    internal func getPredicate(startDate: Date, endDate: Date) -> NSCompoundPredicate {
        let predicateForSamples = HKQuery.predicateForSamples(withStart: startDate, end: endDate)
        let excludeManual = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
        return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateForSamples, excludeManual])
    }
    
    internal func getDescriptor(startDate: Date, endDate: Date, type: HKQuantityType, options: HKStatisticsOptions) -> HKStatisticsCollectionQueryDescriptor {
        let calendar = Calendar(identifier: .gregorian)
        let anchorDate = calendar.date(bySetting: .hour, value: 0, of: startDate)!
        
        var interval = DateComponents()
        interval.day = 1
                    
        return HKStatisticsCollectionQueryDescriptor(
            predicate: HKSamplePredicate.quantitySample(type: type, predicate: getPredicate(startDate: startDate, endDate: endDate)),
            options: options,
            anchorDate: anchorDate,
            intervalComponents: interval
        )
    }

Implementation

    public func observeWalkingActivityInBackground(_ start: Bool, toRead: Set<HKQuantityType>, memberID: String) {
        observeWalkingActivityInBackground(start, toRead: toRead) { [weak self] result in
            guard let self = self else { return }
            
        }
    }
Answered by DTS Engineer in 828682022

HKAnchoredObjectQuery can not register for background delivery, as described in Long-running queries. You can use HKObserverQuery instead.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

HKAnchoredObjectQuery can not register for background delivery, as described in Long-running queries. You can use HKObserverQuery instead.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

HKAnchoredObjectQuery Stops Receiving Updates
 
 
Q