Updated 21 March 2025 for iOS 18.
For a more modern version using SwiftUI to visualise the data, see How to Fetch and Display HealthKit Data with HKAnchoredObjectQuery
The HKAnchoredObjectQuery provides a useful way to keep tabs on what has changed for a particular sample type. It provides us with both the ability to receive a snapshot of data, and then on subsequent calls, a snapshot of what has changed. Alternatively you can create the initial snapshot of samples, and then continuously monitor as a long-running query which notifies you each time a new sample is added or when a sample is deleted.
For this tutorial we will continue on from an earlier tutorial, and add more functions that will allow us to test the HKAnchoredObjectQuery class. Go ahead and download the latest version of the project and open it in Xcode.
Getting Started
To begin with this particular query we need to look at the documentation to see what is required to get going. The first step is to look at the initialiser:
init(type:predicate:anchor:limit:resultsHandler:)
init(type: HKSampleType,
predicate: NSPredicate?,
anchor: HKQueryAnchor?,
limit: Int,
resultsHandler handler: @escaping (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void)
We see that just like the HKSampleQuery, and the other queries we will use, the initialiser for this query requires a HKSampleType. We can use what we used previously:
let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)
We then need to declare an optional predicate, ie, a predicate or just nil. We will use nil for this example.
Next we provide an anchor which is an optional HKQueryAnchor. We will cover this in more detail in a few moments.
We next provide a limit. This is used to limit the number of samples returned by the query. We will use HKObjectQueryNoLimit in this example.
The resultsHandler provides us with the query, the array of returned samples which are marked as option as there might not be anything returned. We are provided with an array of deleted objects (again, optional), then the optional HKQueryAnchor which we can use to monitor when changes are made, and then finally an error.
The HKQueryAnchor
Much of what we have seen is very similar to the HKSampleQuery. There are just a few differences. One difference is that you cannot sort. The other difference is that you are notified of additions or deletions to the sample type.
The HKAnchoredObjectQuery initialiser allows you to optionally pass in a HKQueryAnchor. If this is the first query your app is making, then you can just pass in nil here. This will request all data. When all data is retrieved, a new anchor is provided which is used automatically when the app is running, or alternatively, you can persist the anchor and use it for the next time you open the app.
The documentation tells us that “the anchor object corresponds to the last object that was returned by the previous anchored object query”. We also learn that when new queries are made, only items after the anchor object will be returned.
Building a HKAnchoredObjectQuery
Lets begin by building a query and executing it. We will use the code from the documentation with a couple of small adjustments. We will use bodyMass for this example because we can easily add and remove samples to show how this works. You might also consider using stepCount. Be aware that steps counted on either your watch or phone will not show up immediately in HealthKit. They tend to save and sync every several minutes in small increments.
func testAnchoredQuery() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else {
fatalError("*** Unable to get the body mass type ***")
}
var anchor = HKQueryAnchor.init(fromValue: 0)
if UserDefaults.standard.object(forKey: "Anchor") != nil {
let data = UserDefaults.standard.object(forKey: "Anchor") as! Data
do {
if let unarchivedAnchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data) {
anchor = unarchivedAnchor
} else {
print("Failed to unarchive anchor.")
}
} catch {
print("Error unarchiving anchor: \(error)")
}
}
let query = HKAnchoredObjectQuery(type: bodyMassType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit) { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
fatalError("*** An error occurred during the initial query: \(errorOrNil!.localizedDescription) ***")
}
if let newAnchor = newAnchor {
do {
// Archiving the newAnchor object with the new method
let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "Anchor")
anchor = newAnchor // Assigning the newAnchor
} catch {
print("Error archiving anchor: \(error)")
}
}
for bodyMassSample in samples {
print("Samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
print("Anchor: \(anchor)")
}
healthStore.execute(query)
}
Lines 2-4 we create the quantity type.
Line 6 creates an anchor from value 0. You can think of this as a starting point.
Lines 8 – 20 grabs the anchor from UserDefaults if it exists. This is where we store the anchor so that when we next run a query, we know when to fetch the data from.
Lines 22 to 28 create the query. We pass in the type (which we are calling bodyMassType). We leave predicate at nil, and anchor at anchor. We then specify no limit. This allows us to query the data from an anchor provided.
Lines 30 – 39 is where we store the new anchor in UserDefaults.
Lines 42 onwards we loop over the samples ad deletedObjects and print the results to the screen.
The HKQueryAnchor
When you run this code, you’ll get all body mass samples and then get a new anchor set. We could have narrowed that down a little by passing in a predicate with a start and end date, but essentially, an anchor returns everything within the confines of the predicate from the anchor, if it exists.
You may have noticed that the results handler creates a HKQueryAnchor for us (printed line 50 in the code above). How this works is that we initially pass in a new HKQueryAnchor which returns all objects, and then on any subsequent calls we would pass in the HKQueryAnchor that was handed to us by the most recent results handler.
The anchor does not naturally persist, so unless you persist it between app loads, you will need to start the query from the beginning each time the app loads.
If you run the app now you will see a log of every single bodyMass that has been recorded on your device. When completed you can stop the app and visit the health app. Add in a few new weight entries. Run the app again, and in the logs you will see just the changes that you made. If you stop the app, and go and delete some or all of those entries that you just created, then when starting the app again, you will be notified of the specific objects that were deleted.
This is useful because it allows your app to run more efficiently. If you had a weight tracking app, you would just need to fetch all of the user data once. When you persist the anchor you can then rerun the query with that anchor and receive just the information that has been added or deleted. When that happens, the new anchor will be updated again, and each time you load the app you will only be presented with changes that have been made between the last anchor and now.
Long-Running Query
I mentioned earlier that HKAnchoredObjectQuery also has the ability to be long-running. What this means is that you can make a query just once, and then each time a change is made in Health to body Mass (or whatever type of sample you are running the query on), you will be immediately notified without having to run the query manually.
The changes to the code are relatively simple. HKAnchoredObjectQuery has a property called updateHandler. All we need to do is set this in a specific way and this will start the long-running query:
func testAnchoredQuery() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else {
fatalError("*** Unable to get the body mass type ***")
}
var anchor = HKQueryAnchor(fromValue: 0)
if let data = UserDefaults.standard.data(forKey: "Anchor") {
do {
if let unarchivedAnchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data) {
anchor = unarchivedAnchor
} else {
print("Failed to unarchive anchor.")
}
} catch {
print("Error unarchiving anchor: \(error)")
}
}
let query = HKAnchoredObjectQuery(type: bodyMassType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit) { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
fatalError("*** An error occurred during the initial query: \(errorOrNil!.localizedDescription) ***")
}
if let newAnchor = newAnchor {
anchor = newAnchor
do {
// Archiving the newAnchor object with the modern method
let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "Anchor")
} catch {
print("Error archiving anchor: \(error)")
}
}
for bodyMassSample in samples {
print("Samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
print("Anchor: \(anchor)")
}
query.updateHandler = { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
fatalError("*** An error occurred during an update: \(errorOrNil!.localizedDescription) ***")
}
if let newAnchor = newAnchor {
anchor = newAnchor
do {
// Archiving the newAnchor object with the modern method
let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "Anchor")
} catch {
print("Error archiving anchor: \(error)")
}
}
for bodyMassSample in samples {
print("samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
}
healthStore.execute(query)
}
All changes made begin on line 50. Here we set the updateHandler property and provide it with an update handler. Everything else after that follows the same pattern as the non updateHandler version (which is still kept in tact).
When you now run the app, you’ll notice that you don’t have to stop and start the app to re-run the query. Just hit the home button, go to Health, add a weight record, re-open the app, and you’ll be notified of the change. Hit the home button again, open Health, delete the weight record, re-open the app, and the deleted message will appear.
The updateHandler does not work in the background, but it does persist between loads. As soon as you open your app you will be immediately notified of any changes that have been made. This is different to the non-updateHandler version where the query had to be run (which is why we previously stopped and started the app with Xcode).
You can find the full code below, and download the project here.
//
// ViewController.swift
// HealthKit Queries
//
// Created by Matthew Newill on 10/07/2018.
// Copyright © 2025 DevFright. All rights reserved.
//
import UIKit
import HealthKit
class ViewController: UIViewController {
let healthStore = HKHealthStore()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if HKHealthStore.isHealthDataAvailable() {
let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!,
HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex)!,
HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.dateOfBirth)!,
HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)!]
let writeDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!]
healthStore.requestAuthorization(toShare: writeDataTypes, read: readDataTypes) { (success, error) in
if !success {
// Handle the error here.
} else {
self.testCharachteristic()
self.testSampleQuery()
self.testSampleQueryWithPredicate()
self.testSampleQueryWithSortDescriptor()
self.testAnchoredQuery()
}
}
}
}
// Fetches biologicalSex of the user, and date of birth.
func testCharachteristic() {
// Biological Sex
if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.female {
print("You are female")
} else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.male {
print("You are male")
} else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.other {
print("You are not categorised as male or female")
}
// Date of Birth
if #available(iOS 10.0, *) {
try! print(healthStore.dateOfBirthComponents())
} else {
// Fallback on earlier versions
do {
let dateOfBirth = try healthStore.dateOfBirth()
print(dateOfBirth)
} catch let error {
print("There was a problem fetching your data: \(error)")
}
}
}
// HKSampleQuery with a nil predicate
func testSampleQuery() {
// Simple Step count query with no predicate and no sort descriptors
let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)
let query = HKSampleQuery.init(sampleType: sampleType!,
predicate: nil,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil) { (query, results, error) in
print(results)
}
healthStore.execute(query)
}
// HKSampleQuery with a predicate
func testSampleQueryWithPredicate() {
let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)
let today = Date()
let startDate = Calendar.current.date(byAdding: .month, value: -3, to: today)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: today, options: HKQueryOptions.strictEndDate)
let query = HKSampleQuery.init(sampleType: sampleType!,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil) { (query, results, error) in
print(results)
}
healthStore.execute(query)
}
// Sample query with a sort descriptor
func testSampleQueryWithSortDescriptor() {
let sampleType = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)
let today = Date()
let startDate = Calendar.current.date(byAdding: .month, value: -3, to: today)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: today, options: HKQueryOptions.strictEndDate)
let query = HKSampleQuery.init(sampleType: sampleType!,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]) { (query, results, error) in
print(results)
}
healthStore.execute(query)
}
func testAnchoredQuery() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else {
fatalError("*** Unable to get the body mass type ***")
}
var anchor = HKQueryAnchor.init(fromValue: 0)
if let data = UserDefaults.standard.data(forKey: "Anchor") {
do {
anchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data) ?? HKQueryAnchor.init(fromValue: 0)
} catch {
print("Failed to unarchive anchor: \(error)")
}
}
let query = HKAnchoredObjectQuery(type: bodyMassType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit) { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
fatalError("*** An error occurred during the initial query: \(errorOrNil!.localizedDescription) ***")
}
if let newAnchor = newAnchor {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "Anchor")
} catch {
print("Failed to archive anchor: \(error)")
}
anchor = newAnchor
}
for bodyMassSample in samples {
print("Samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
print("Anchor: \(anchor)")
}
query.updateHandler = { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
// Handle the error here.
fatalError("*** An error occurred during an update: \(errorOrNil!.localizedDescription) ***")
}
if let newAnchor = newAnchor {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "Anchor")
} catch {
print("Failed to archive anchor: \(error)")
}
anchor = newAnchor
}
for bodyMassSample in samples {
print("samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
}
healthStore.execute(query)
}
func testAnchoredQuery1() {
guard let bodyMassType = HKObjectType.quantityType(forIdentifier: .bodyMass) else {
fatalError("*** Unable to get the body mass type ***")
}
var anchor = HKQueryAnchor(fromValue: 0)
if let data = UserDefaults.standard.data(forKey: "Anchor") {
do {
if let unarchivedAnchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data) {
anchor = unarchivedAnchor
} else {
print("Failed to unarchive anchor.")
}
} catch {
print("Error unarchiving anchor: \(error)")
}
}
let query = HKAnchoredObjectQuery(type: bodyMassType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit) { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
fatalError("*** An error occurred during the initial query: \(errorOrNil!.localizedDescription) ***")
}
if let newAnchor = newAnchor {
anchor = newAnchor
do {
// Archiving the newAnchor object with the modern method
let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "Anchor")
} catch {
print("Error archiving anchor: \(error)")
}
}
for bodyMassSample in samples {
print("Samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
print("Anchor: \(anchor)")
}
query.updateHandler = { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in
guard let samples = samplesOrNil, let deletedObjects = deletedObjectsOrNil else {
fatalError("*** An error occurred during an update: \(errorOrNil!.localizedDescription) ***")
}
if let newAnchor = newAnchor {
anchor = newAnchor
do {
// Archiving the newAnchor object with the modern method
let data = try NSKeyedArchiver.archivedData(withRootObject: newAnchor, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "Anchor")
} catch {
print("Error archiving anchor: \(error)")
}
}
for bodyMassSample in samples {
print("samples: \(bodyMassSample)")
}
for deletedBodyMassSample in deletedObjects {
print("deleted: \(deletedBodyMassSample)")
}
}
healthStore.execute(query)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
biswajit neog says
Hello Sir
I would sincerely Thank you for creating this Blog. It has been of immense help. I was so lost, frustrated and annoyed at the level of documentation Apple provides. It is pathetic.
But your blog is like the beacon from a lighthouse saving ship many from the rocky shores
Thank you once again, sincerely.