HKAnchoredObjectQuery: new samples, deleted objects and the HKQueryAnchor
Last verified October 3, 2026 · 6 min read
Covered here:HealthKit
HKAnchoredObjectQuery is the class that makes incremental sync possible on iOS, and the only query class that tells you a sample was deleted. This page covers how it works according to Apple's documentation. The sync design built on top of it, including what to do with a deletion once you have it, lives in incremental sync, and a tested implementation of the save-the-cursor loop is in the incremental sync cookbook recipe.
What Apple says it is#
The abstract: "A query that returns changes to the HealthKit store, including a snapshot of new changes and continuous monitoring as a long-running query." The overview explains the mechanism: "An HKAnchoredObjectQuery returns an anchor value that corresponds to the last sample or deleted object received by that query. Subsequent queries can use this anchor to restrict their results to only newer saved or deleted objects."
Apple also describes it as two queries in one: it "grabs a snapshot of the data currently stored in the HealthKit store (like an HKSampleQuery)" and "can also perform a long-running query that responds to updates (like an HKObserverQuery)". Hence the advice that "you may want to use anchored object queries, even when you aren't using anchors to limit the results. In this case, set the anchor parameter to nil."
The query is "mostly immutable": you can assign updateHandler after creating it, and nothing else.
Availability. The current initializer, init(type:predicate:anchor:limit:resultsHandler:), is listed from iOS 9.0, iPadOS 9.0, Mac Catalyst 13.0, macOS 13.0, visionOS 1.0 and watchOS 2.0, as are HKQueryAnchor and HKDeletedObject. An older initializer that took an Int anchor and returned no deleted objects is marked deprecated, with Apple's note to use init(type:predicate:anchor:limit:resultsHandler:) instead.
The initializer#
| Parameter | What Apple says |
|---|---|
type | "This query supports all subclasses of HKSampleType, such as HKQuantityType, HKCategoryType, HKWorkoutType, and HKCorrelationType." |
predicate | "A predicate that filters both the samples and the deleted objects returned by the query." |
anchor | "The new query returns only samples and deleted objects that are newer than that object. Pass nil to receive all the matching samples and recently deleted objects currently in the HealthKit store." |
limit | "To receive all of the new samples, pass HKObjectQueryNoLimit." |
resultsHandler | Receives the query, the samples, the deleted objects, a new anchor and an error. Both arrays are nil if an error occurred. |
The handler runs on a background queue; Apple says to "dispatch these results to the main queue before updating the user interface."
The anchor#
HKQueryAnchor is "An object used to identify all the samples previously returned by an anchored object query." Treat it as opaque: it is a position, not a timestamp, and the class offers nothing to read out of it. The handler's newAnchor is "An anchor object corresponding to the last object returned by this query."
Apple's documentation for the async descriptor version carries a tip that applies equally here: "Because HKQueryAnchor instances adopt the NSSecureCoding protocol, you can save the most recent anchor and use it the next time your app launches."
There is one constructor, init(fromValue:), and it exists for migration. Apple: "Prior to iOS 9.0, anchored object queries used anchor values to track the last sample returned by a previous query. Use this method to convert those anchor values into anchor objects."
Deleted objects#
HKDeletedObject is "An object that represents a sample that has been deleted from the HealthKit store." It carries two documented properties, uuid and metadata, so a deletion tells you which object went, not when it happened or what value it held.
The catch is on the same page: "Deleted objects are temporary; the system may remove them from the HealthKit store at any time to free up space." Apple's prescription for not missing any is to combine three things: "create an HKObserverQuery and register it for background delivery", then, because the observer "does not provide a list of deleted objects", "use the observer query's update handler to create an anchored object query for the newly deleted objects." That pairing is the subject of the HKObserverQuery page.
A minimal incremental read#
import HealthKit
func syncSteps(store: HKHealthStore, from savedAnchor: HKQueryAnchor?) {
let stepType = HKQuantityType(.stepCount)
let query = HKAnchoredObjectQuery(type: stepType,
predicate: nil,
anchor: savedAnchor, // nil on the very first run
limit: HKObjectQueryNoLimit) { _, added, deleted, newAnchor, error in
guard let added, let deleted else { return } // both nil only when error is set
apply(added: added, deleted: deleted) // your code
if let newAnchor { save(newAnchor) } // your code: persist only after applying
}
store.execute(query)
}
apply and save stand in for your own persistence. The order matters: if the app dies between saving the anchor and applying the changes, those changes are skipped for good. The cookbook recipe covers that failure in detail.
Keeping it running#
Set updateHandler before execute(_:) and the query becomes long-running. Apple: "If this property is set to nil, the anchor query automatically stops as soon as it finishes calculating the initial results." Otherwise "The system executes the update handler on a background queue whenever matching samples are saved to or deleted from the HealthKit store." Its signature matches the results handler, including a fresh anchor on every call, so save each one. Stop the query with the store's stop(_:).
Reading in batches#
A non-nil limit turns the query into a pager. Because each newAnchor corresponds to the last object returned, running the query again with that anchor continues where the last batch stopped. Apple's documentation for HKAnchoredObjectQueryDescriptor describes exactly this pattern for large histories: start with a nil anchor, read a batch, then "start a new one-shot query for the next batch. Continue reading batches until there's no new data." The async version is covered on the query descriptors page.
Pitfalls#
- Expecting a deletion to carry a date.
HKDeletedObjectdocuments onlyuuidandmetadata. If you need to know which day a deletion affects, keep your own record of what each UUID was; incremental sync works through that. - Relying on deleted objects lingering. Apple says the system may remove them at any time. An app that only runs its anchored query occasionally can miss deletions unless the observer and background delivery pairing is in place.
- Summing the added samples into a total. An anchored query reports changes, not totals. Recompute affected periods with HKStatisticsQuery instead of incrementing a counter.
- Forgetting that the predicate filters deletions too. A date-window predicate that excludes a sample also excludes its deletion. A predicate is fine for a bounded view, but a full sync usually wants
nil. - Not saving the anchor from the update handler. Each update hands you a new anchor. Keep only the first and the next launch replays everything since then.
Frequently asked questions
- What anchor should I pass to HKAnchoredObjectQuery the first time it runs?
- Pass nil. Apple documents that a nil anchor returns all the matching samples and recently deleted objects currently in the HealthKit store, and its discussion says to pass nil the first time, then save the returned anchor and pass it to the next query. Every later run with that saved anchor returns only samples and deleted objects newer than it.
- Can I save an HKQueryAnchor and reuse it the next time my app launches?
- Yes. Apple's documentation for the anchored object query descriptor notes that HKQueryAnchor instances adopt the NSSecureCoding protocol, so you can save the most recent anchor and use it the next time your app launches. Persist the new anchor only after the changes it covers are safely applied, and save the anchor delivered with every update, not only the first one.
- Does HealthKit keep HKDeletedObject records forever?
- No. Apple states that deleted objects are temporary and that the system may remove them from the HealthKit store at any time to free up space. To avoid missing deletions, Apple recommends registering an HKObserverQuery for background delivery and running an anchored object query from its update handler, since the observer itself does not list what was deleted.
- Does the predicate on an HKAnchoredObjectQuery filter deleted objects as well as samples?
- Yes. Apple describes the predicate parameter as one that filters both the samples and the deleted objects the query returns. A date-window predicate therefore hides the deletion of any sample outside that window, which is why a full sync usually passes nil and narrows the data afterwards.
- What is the HKQueryAnchor init(fromValue:) initializer for?
- Migrating from the old integer anchors. Apple explains that before iOS 9.0 anchored object queries tracked the last returned sample with an integer anchor value, and init(fromValue:) converts one of those values into an HKQueryAnchor object that the current initializer accepts. New code never needs it, because the query hands you HKQueryAnchor objects directly.
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