HKStatisticsCollectionQuery: daily totals, anchorDate and intervalComponents
Last verified October 3, 2026 · 6 min read
Covered here:HealthKit
If you are drawing a chart of steps per day, energy per week or heart rate per hour, this is the class. It is one HKStatisticsQuery per interval, run for you, with the same options and the same accessor rules. What it adds is a way of defining the intervals that most people get slightly wrong the first time.
What Apple says it is#
The abstract: "A query that performs multiple statistics queries over a series of fixed-length time intervals." Apple's overview gives the use case directly: "you might create a statistics collection query that calculates the total number of steps for each day or the average heart rate for each hour."
From the same page:
- "You can only use statistics collection queries with quantity samples."
- "Statistics collection queries are mostly immutable. You can assign the query's initialResultsHandler and statisticsUpdateHandler properties after instantiating the object. You must set all other properties when you instantiate the object, and they can't change."
- "Like observer queries, collection queries can also act as long-running queries, receiving updates when the HealthKit store's content changes."
Availability. Apple's class page lists iOS, iPadOS, Mac Catalyst, macOS, visionOS and watchOS with no introduction version in its documentation data. The result type, HKStatisticsCollection, is listed from iOS 8.0, iPadOS 8.0, Mac Catalyst 13.0, macOS 13.0, visionOS 1.0 and watchOS 2.0.
anchorDate and intervalComponents#
The initializer is init(quantityType:quantitySamplePredicate:options:anchorDate:intervalComponents:). The two time arguments are the whole design.
intervalComponents is "The date components that define the time interval for each statistics object in the collection." A day is DateComponents(day: 1); Apple's article uses DateComponents(day: 7) for weeks.
anchorDate is where people stumble, because it reads like "the start of my range" and is nothing of the kind. Apple's description: "Technically, the anchor sets the start time for a single time interval. All other time intervals must align with this interval. The time intervals can extend before or after the anchor date. Each time interval has the same length, and there is no gap between adjacent intervals. Think of time as a number line: The anchor date represents its origin, with the intervals creating tick marks that extend away from the origin in both directions."
The article makes the point with an example: with a one-day interval, "The exact date for the anchor point doesn't matter. It could be 3:34 a.m., January 1, 1970 or 3:34 a.m., March 15, 2065. In both cases, the statistics collection query partitions the matching samples into days, starting each day at 3:34 a.m." So for calendar days, anchor on any midnight. For weeks starting Monday, anchor on any Monday at the hour you want weeks to begin; Apple's own sample anchors on Monday at 3:00 a.m.
What limits the range is the predicate, and what you read back is chosen when you enumerate. The anchor only sets the grid.
The two handlers#
Unlike most query classes, you do not pass a closure to the initializer. You assign one or both properties before calling execute(_:):
initialResultsHandler: "The results handler for the query's initial results." It receives the query, anHKStatisticsCollectionand an error.statisticsUpdateHandler: "The results handler for monitoring updates to the HealthKit store." Apple: "If this property is set to nil, the statistics collection query will automatically stop as soon as it has finished calculating the initial results." If it is set, the query keeps running, and "If any new, matching samples are saved to the store—or if any of the existing matching samples are deleted from the store—the query executes the update handler on a background queue."
A query with an update handler runs until you call the store's stop(_:) method.
Reading the collection#
HKStatisticsCollection gives you three ways in, and they treat empty intervals differently:
| Method | What Apple documents |
|---|---|
statistics() | One statistics object "for each time interval that has at least one sample". "This method ignores time intervals that do not have any samples." |
enumerateStatistics(from:to:with:) | Calls the block "once for each time interval between the start and end dates", in chronological order. "If there are no samples for a particular time interval, the corresponding statistic object has a nil-valued quantity." |
statistics(for:) | The object for the interval containing a date; with no samples in that interval, "the statistics object has a nil-valued quantity." |
For a chart with one bar per day, enumerateStatistics(from:to:with:) is the one that gives you every day.
Daily step totals#
import HealthKit
func readDailySteps(store: HKHealthStore, days: Int, calendar: Calendar = .current) {
let stepType = HKQuantityType(.stepCount)
let endDate = Date()
let today = calendar.startOfDay(for: endDate)
guard let startDate = calendar.date(byAdding: .day, value: -(days - 1), to: today) else { return }
let window = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
let query = HKStatisticsCollectionQuery(quantityType: stepType,
quantitySamplePredicate: window,
options: .cumulativeSum,
anchorDate: today, // any midnight works
intervalComponents: DateComponents(day: 1))
query.initialResultsHandler = { _, collection, error in
guard let collection else { return } // inspect error here
collection.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
let day = statistics.startDate
let steps = statistics.sumQuantity()?.doubleValue(for: .count()) // nil: no samples that day
// record (day, steps) for the chart
}
DispatchQueue.main.async {
// redraw the chart
}
}
store.execute(query)
}
stepCount is a cumulative type, so it takes cumulativeSum; a discrete type such as heart rate would take discreteAverage, discreteMin or discreteMax. The accessor rules are the same as for HKStatisticsQuery.
Errors worth handling#
Apple's article singles out one: "if your app can run a query in the background, you need to check for and handle the HKError.Code.errorDatabaseInaccessible error", and its sample comments that this means HealthKit could not access the database because the device is locked. That error has its own page: HealthKit database inaccessible.
Pitfalls#
- Using statistics() for a chart. It skips empty intervals, so the array can have, in Apple's words, "arbitrarily large gaps in time between adjacent statistics objects". Zip it against a list of days and the bars shift. Enumerate instead.
- Treating the anchor as the range start. The anchor only sets where intervals begin. A predicate, and the dates you pass to
enumerateStatistics(from:to:with:), decide the range. - Forgetting to stop it. With
statisticsUpdateHandlerset, the query keeps running untilstop(_:). Starting one per screen appearance without stopping the last one leaves several running. - Assigning handlers late. Apple's sequence is to instantiate, set the handlers, then execute. Set them before
execute(_:). - Daylight-saving days. Apple's description says every interval has the same length. Its pages for this class do not discuss what a one-day interval does across a clock change or a time-zone move, so test those days on a device. Timezones and day boundaries covers the design side.
- Slow first results. As with every statistics query, Apple warns that "Statistical calculations can take a considerable amount of time, especially if there are a large number of samples involved." Pass a predicate rather than
nilwhen you only chart recent weeks.
Frequently asked questions
- What should anchorDate be for daily totals in HKStatisticsCollectionQuery?
- Any date whose time of day is when you want each day to begin, usually a midnight. Apple explains that the anchor sets the start of one interval and every other interval aligns with it, extending in both directions, so the exact date does not matter: its example says 3:34 a.m. on January 1, 1970 and 3:34 a.m. on March 15, 2065 both produce days that start at 3:34 a.m. The range you get back is set by the predicate and the dates you enumerate, not by the anchor.
- Why are some days missing from statistics() on an HKStatisticsCollection?
- Because statistics() returns objects only for intervals that contain at least one matching sample. Apple documents that it ignores intervals with no samples, so the array can have arbitrarily large gaps between adjacent objects. Use enumerateStatistics(from:to:with:) instead, which Apple says calls your block once for every interval between the two dates, passing a statistics object with a nil-valued quantity where there were no samples.
- Does HKStatisticsCollectionQuery keep running after it returns the initial results?
- Only if you set statisticsUpdateHandler. Apple documents that when that property is nil the query stops automatically once it has calculated the initial results. When it is set, the query keeps monitoring the store and calls the handler on a background queue whenever matching samples are saved or deleted, until you call the health store's stop(_:) method.
- Why does the HKStatisticsCollectionQuery initializer have no results handler?
- Because this class takes its handlers as properties instead. Apple's article notes that unlike most other queries, you do not set the callback when you instantiate a statistics collection query; you assign initialResultsHandler, statisticsUpdateHandler or both after creating it and before calling execute(_:). Those two properties are the only ones Apple allows you to change after initialization.
Keep reading
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