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 ***") } // Create the query. let query = HKAnchoredObjectQuery(type: bodyMassType, predicate: nil, anchor: nil, 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) ***") } for bodyMassSample in samples { print("Samples: \(bodyMassSample)") } for deletedBodyMassSample in deletedObjects { print("deleted: \(deletedBodyMassSample)") } } healthStore.execute(query) }
Lines 2-4 we create the quantity type.
Lines 7-10 contain the query. We pass in the type (which we are calling bodyMassType). We leave predicate at nil, and anchor at nil. We then specify no limit.
Line 9 we are passing nil. This is because on this particular test we are not using the anchor. We are simply using the query just like the HKSampleQuery, just to see how it works.
Lines 11-13 we create 2 constants, one for the samples and another for the deleted items.
Line 15-17 is where we iterate through the received samples (unless nil was passed). We print out each item that was returned in the array.
Line 19-21 will not actually run in this configuration. Because we passed the anchor as nil, it has no way at this point to let you know what items have been deleted. This leads on to the next section which is how to use the anchor.
The HKQueryAnchor
In the last section we passed in nil for the anchor. This resulted in all bodyMass records being returned. We could have narrowed that down a little by passing in a predicate with a start and end date, but essentially, a nil anchor returns everything within the confines of the predicate, if it exists. To use the HKAnchoredObjectQuery with an anchor, it requires that we pass in a HKQueryAnchor.
You may have noticed that the results handler creates a HKQueryAnchor for us. 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. We will first look at how to keep a copy of HKQueryAnchor, and then look at how we can make edits and then reinitialise HKAnchoredObjectQuery to get just a list of changes. We can do this with the following code:
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 anchor = NSKeyedUnarchiver.unarchiveObject(with: data) as! HKQueryAnchor } 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) ***") } anchor = newAnchor! let data : Data = NSKeyedArchiver.archivedData(withRootObject: newAnchor as Any) UserDefaults.standard.set(data, forKey: "Anchor") for bodyMassSample in samples { print("Samples: \(bodyMassSample)") } for deletedBodyMassSample in deletedObjects { print("deleted: \(deletedBodyMassSample)") } print("Anchor: \(anchor)") } healthStore.execute(query) }
The changes start on line 6. Here we create a HKQueryAnchor and initialise it with a zero. This gives us an anchor that we can use later on.
Lines 8-11 are where we check if we already have an anchor saved. If we do, we need first extract the data and store it as Data. We then unarchive that data with NSKeyedUnarchiver. We need to go this route because we cannot store a HKQueryAnchor in UserDefaults as it is. Instead, we need to convert to data and archive that data, and then unarchive it when needed.
We next create the HKAnchoredObjectQuery, but instead of setting the anchor to nil, we pass in our newly created anchor which will either be initialised with a 0, or be initialised later on in the code.
Lines 17-19 are the same as the previous version earlier on in this tutorial.
Line 21 is where we work more with the anchor. Here we assign the new anchor to anchor. This means that the next time the query runs it ignores everything except changes that have happened since the anchor was created.
Lines 22 and 23 are where we archive the newAnchor to Data, and then save that in UserDefaults. This is where the anchor is persisted.
The lines after this are as the previous version earlier on in the tutorial.
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.init(fromValue: 0) if UserDefaults.standard.object(forKey: "Anchor") != nil { let data = UserDefaults.standard.object(forKey: "Anchor") as! Data anchor = NSKeyedUnarchiver.unarchiveObject(with: data) as! HKQueryAnchor } 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) ***") } anchor = newAnchor! let data : Data = NSKeyedArchiver.archivedData(withRootObject: newAnchor as Any) UserDefaults.standard.set(data, forKey: "Anchor") 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) ***") } anchor = newAnchor! let data : Data = NSKeyedArchiver.archivedData(withRootObject: newAnchor as Any) UserDefaults.standard.set(data, forKey: "Anchor") for bodyMassSample in samples { print("samples: \(bodyMassSample)") } for deletedBodyMassSample in deletedObjects { print("deleted: \(deletedBodyMassSample)") } } healthStore.execute(query) }
All changes made begin on line 36. 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.
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 UserDefaults.standard.object(forKey: "Anchor") != nil { let data = UserDefaults.standard.object(forKey: "Anchor") as! Data anchor = NSKeyedUnarchiver.unarchiveObject(with: data) as! HKQueryAnchor } 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) ***") } anchor = newAnchor! let data : Data = NSKeyedArchiver.archivedData(withRootObject: newAnchor as Any) UserDefaults.standard.set(data, forKey: "Anchor") 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) ***") } anchor = newAnchor! let data : Data = NSKeyedArchiver.archivedData(withRootObject: newAnchor as Any) UserDefaults.standard.set(data, forKey: "Anchor") 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.