Skip to content
AF
HealthKit Queries

HealthKit async/await queries: HKSampleQueryDescriptor and the other descriptors

Last verified October 3, 2026 · 5 min read

HealthKit's query descriptors, such as HKSampleQueryDescriptor, HKAnchoredObjectQueryDescriptor and HKStatisticsCollectionQueryDescriptor, are Swift structures that run HealthKit queries with async/await instead of callbacks. Each takes HKSamplePredicate values that carry the sample type, so results come back typed, for example as an array of HKQuantitySample rather than of the base HKSample class. Call result(for:) for a one-shot snapshot, or results(for:) where a descriptor offers it for a long-running query that streams updates as an AsyncSequence. Apple lists them from iOS 15.4, iPadOS 15.4, Mac Catalyst 15.4, macOS 13.0 and watchOS 8.5.

Covered here:HealthKit

The callback query classes still work, but they hand results to closures on a background queue. The descriptors return them through async/await instead, and type them along the way. This page covers what each descriptor returns and the places where the async version behaves differently from the class it wraps.

What they are#

Apple's description of HKSampleQueryDescriptor is "A query interface that reads samples using Swift concurrency." The relationship to the class is stated plainly: "When you call the descriptor's result(for:) method, it creates and executes an HKSampleQuery in the background, passing the results from the query's resultsHandler as its return value." Apple spells this relationship out for the sample descriptor; for each descriptor, the class page in this section covers the matching callback query.

Availability. Every descriptor on this page is listed from iOS 15.4, iPadOS 15.4, Mac Catalyst 15.4, macOS 13.0 and watchOS 8.5. Apple's documentation data also lists visionOS for them, without an introduction version. If you support anything earlier than iOS 15.4, you still need the callback classes.

The descriptors and what they return#

DescriptorOne-shot result(for:) returnsLong-running results(for:)
HKSampleQueryDescriptor[Sample]not offered
HKStatisticsQueryDescriptorHKStatistics?not offered
HKStatisticsCollectionQueryDescriptorHKStatisticsCollectionyes; each element carries statisticsCollection and updatedStatistics
HKAnchoredObjectQueryDescriptora Result with addedSamples, deletedObjects and newAnchoryes
HKActivitySummaryQueryDescriptor[HKActivitySummary]yes
HKWorkoutRouteQueryDescriptornot offereda finite sequence of locations

The route descriptor is the odd one out. Apple: "While this method returns an AsyncSequence, unlike the long-running queries, this sequence has a finite size. Iterating over the sequence asynchronously returns the route's locations, automatically terminating after you receive all the locations." See HKWorkoutRouteQuery.

HKSamplePredicate: the type travels with the filter#

Descriptors do not take a bare NSPredicate. They take HKSamplePredicate, which Apple describes as bundling "an HKSampleType and an optional NSPredicate". Because the structure is generic, "any query that you build using this structure returns properly typed results." You build one with a factory method such as quantitySample(type:predicate:), categorySample(type:predicate:), workout(_:) or workoutRoute(_:), and the date window inside is the same HKQuery.predicateForSamples(withStart:end:options:) covered under query predicates.

A one-shot read#

init(predicates:sortDescriptors:limit:) takes an array of sample predicates, an array of SortDescriptor values and an optional limit. Apple: "If you don't specify the limit, the system returns all matching samples in the HealthKit store", and for sorting, "If you don't need the results in a specific order, pass an empty array."

import HealthKit

func latestSteps(store: HKHealthStore) async throws -> [HKQuantitySample] {
    let stepType = HKQuantityType(.stepCount)
    let descriptor = HKSampleQueryDescriptor(
        predicates: [.quantitySample(type: stepType)],
        sortDescriptors: [SortDescriptor(\.endDate, order: .reverse)],
        limit: 10)
    return try await descriptor.result(for: store)   // typed as [HKQuantitySample]
}

This mirrors Apple's own example. The statistics equivalent is HKStatisticsQueryDescriptor(predicate:options:), whose result is optional: Apple's signature returns HKStatistics?, and its page does not say when it is nil.

Reading a large history in batches#

HKAnchoredObjectQueryDescriptor is how Apple suggests reading a lot of data without loading it at once: "Start with a nil-valued anchor, and create a one-shot query descriptor that reads a batch of data. After you process the results from one query, start a new one-shot query for the next batch. Continue reading batches until there's no new data."

func readAllSteps(store: HKHealthStore) async throws -> HKQueryAnchor? {
    let stepType = HKQuantityType(.stepCount)
    var anchor: HKQueryAnchor? = nil

    while true {
        let descriptor = HKAnchoredObjectQueryDescriptor(
            predicates: [.quantitySample(type: stepType)],
            anchor: anchor,
            limit: 100)
        let batch = try await descriptor.result(for: store)
        anchor = batch.newAnchor
        // process batch.addedSamples and batch.deletedObjects here
        if batch.addedSamples.isEmpty && batch.deletedObjects.isEmpty { break }
    }
    return anchor   // persist it; HKQueryAnchor adopts NSSecureCoding
}

The stop condition here differs from Apple's printed sample on purpose; see the pitfalls below.

Long-running queries as a sequence#

Where a descriptor offers results(for:), it returns an AsyncSequence. For the anchored descriptor, Apple says "The first result represents any changes currently in the HealthKit store, and additional results represent changes as they occur." The statistics collection descriptor behaves the same way, and each element's updatedStatistics is populated "When possible" to show which intervals changed.

Apple's examples run the loop inside a Task stored in a property named updateTask. Apple's pages for these descriptors do not document how cancelling that task ends the underlying HealthKit query, so if you depend on the query stopping, verify it on a device.

Pitfalls#

  • Copying Apple's batch loop as printed. The sample on Apple's HKAnchoredObjectQueryDescriptor page loops while (results.addedSamples != []) && (results.deletedObjects != []). With &&, the loop ends as soon as either array is empty, so a batch with new samples and no deletions stops the read early. To match the stated intent, "Continue reading batches until there's no new data", stop only when both are empty, as the sketch above does.
  • Assuming iOS 15.0. The descriptors are listed from iOS 15.4 and watchOS 8.5, later than the iOS 15.0 multi-descriptor initializers on the callback classes.
  • Passing nil for sort descriptors. The descriptor takes an array; Apple says to pass an empty one when order does not matter.
  • Relying on updatedStatistics. Apple qualifies it with "When possible". Treat statisticsCollection as the source of truth and updatedStatistics as a hint.
  • Mixing types and expecting precise typing. A descriptor's result type comes from its predicates. Apple says to provide one sample predicate per type when querying several, and the generic sample(type:predicate:) factory returns HKSamplePredicate<HKSample>, so mixed reads come back as the base type.

Frequently asked questions

What is the difference between result(for:) and results(for:) on a HealthKit query descriptor?
result(for:) runs a one-shot query and asynchronously returns a snapshot of the current matching results, as Apple describes it. results(for:), offered by the anchored object, statistics collection and activity summary descriptors, starts a long-running query that returns an asynchronous sequence: the first element reflects what is in the store now and later elements arrive as data changes. The workout route descriptor offers only results(for:), and its sequence ends once every location has been returned.
Which OS versions support the HealthKit query descriptors?
Apple's documentation lists HKSampleQueryDescriptor, HKAnchoredObjectQueryDescriptor, HKStatisticsQueryDescriptor, HKStatisticsCollectionQueryDescriptor, HKActivitySummaryQueryDescriptor and HKWorkoutRouteQueryDescriptor from iOS 15.4, iPadOS 15.4, Mac Catalyst 15.4, macOS 13.0 and watchOS 8.5, with visionOS listed without a version. Apps that support earlier releases need the callback query classes on those versions.
Does HKSampleQueryDescriptor run an HKSampleQuery underneath?
Yes. Apple's documentation for HKSampleQueryDescriptor says that calling its result(for:) method creates and executes an HKSampleQuery in the background and returns the results from that query's results handler. The behaviour documented for HKSampleQuery, such as how its predicate matches samples, therefore carries over to the descriptor.
How do I read several HealthKit data types with one query descriptor?
Pass one HKSamplePredicate per type in the predicates array. Apple's documentation for the sample and anchored object descriptors says to provide a sample predicate for each type when querying for multiple types of data. Because the result type follows the predicates, mixing types generally means using the generic sample(type:predicate:) factory, which produces HKSamplePredicate of HKSample, and casting the results yourself.

All 40 questions in HealthKit Queries →

Keep reading

Elsewhere on the site

Pages that share this one’s concepts and sources, from other sections.

Next steps

Was this page useful?

Independent comparison, last reviewed October 3, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.

← All healthkit queries · by AIFitnessAPI