I need my app to sync between HealthKit and our database while it's in the background. I just can't wrap my head around the logic that determines how and when the HKObserverQueries run their updateHandlers. I need data of various different sample types, so I assume I need an observer query for each one. Right?
According to Apple about function enableBackgroundDeliveryForType, "HealthKit wakes your app whenever new samples of the specified type are saved to the store." But if I enable background deliveries and execute observerqueries for, say, blood glucose and weight, they both seem to run their updatehandlers whenever I input data in either one of them in the Health app. This also seems to happen even when I enable background delivery for only one of the sample types. Why?
func startObserving(completion: ((success: Bool) -> Void)!) {
let sampleTypeBloodGlucose = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)!
let sampleTypeWeight = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass)!
// Enable background delivery for blood glucose
self.healthKitStore.enableBackgroundDeliveryForType(sampleTypeBloodGlucose, frequency: .Immediate) {
(success, error) in
if error != nil {
abort()
}
}
// Enable background delivery for weight
self.healthKitStore.enableBackgroundDeliveryForType(sampleTypeWeight, frequency: .Immediate) {
(success, error) in
if error != nil {
abort()
}
}
// Define update handlers for background deliveries
let updateHandlerBloodGlucose: (HKObserverQuery, HKObserverQueryCompletionHandler, NSError?) -> Void = {
query, completionHandler, error in
if error != nil {
abort()
}
// Handle data and call the completionHandler
completionHandler()
}
let updateHandlerWeight: (HKObserverQuery, HKObserverQueryCompletionHandler, NSError?) -> Void = {
query, completionHandler, error in
if error != nil {
abort()
}
// Handle data and call the completionHandler
completionHandler()
}
let observerQueryBloodGlucose = HKObserverQuery(sampleType: sampleTypeBloodGlucose, predicate: nil, updateHandler: updateHandlerBloodGlucose)
healthKitStore.executeQuery(observerQueryBloodGlucose)
let observerQueryWeight = HKObserverQuery(sampleType: sampleTypeWeight, predicate: nil, updateHandler: updateHandlerWeight)
healthKitStore.executeQuery(observerQueryWeight)
completion(success: true)
}
If you are using the background delivery feature of HealthKit, then yes, you do need to open an HKObserverQuery for each type of data you observe and handle the invocations of the updateHandler and call the provided completion when you are finished. However, the updateHandler of HKObserverQuery is advisory and invocations do not necessarily correspond one-to-one with changes to the HealthKit database (there is not always enough information available to determine what your app has processed and what it hasn't, so sometimes the handler may run when there isn't new data).
Don't worry about understanding or controlling precisely when updateHandler runs - just use it as a trigger to perform other queries that will actually give you up-to-date values from HealthKit. If you need to know precisely which samples in HealthKit are new, for example, then your app should use an HKAnchoredObjectQuery.
Related
For my App on Apple Watch I want to observe when a workout in Workout-App was completed, to do a background update of my App and re-render the Complication. But the Query never fires.
My Approach was to use an HKObserverQuery and observe .workoutType(). Documentation mentions:
HealthKit uses workout types to create samples that store information about individual workouts.
On https://developer.apple.com/documentation/healthkit/hkobjecttype/1615132-workouttype. So that felt like the right sampleType.
I added com.apple.developer.healthkit.background-delivery to the Entitlements of the WatchApp Extension and Workout access permissions are setup properly.
That is the code, I am using:
let query = HKObserverQuery(sampleType: .workoutType(), predicate: nil, updateHandler: { [weak self] _, completionHandler, error in
guard error == nil else {
// CoreInsights.logs.track("HK Query Error", level: .error)
return
}
// CoreInsights.logs.track("Update Trigger HK", level: .info)
// self?.performBackgroundTasks(completion: {
// completionHandler()
// })
})
self?.healthStore.execute(query)
self?.healthStore.enableBackgroundDelivery(for: .workoutType(), frequency: .immediate, withCompletion: { updateSuccess, _ in
// CoreInsights.logs.track("HK Background \(updateSuccess)", level: .info)
})
healthStore.enableBackgroundDelivery returns with success true.
Someone knows, if I am using the right type and setup background correctly?
I use coreData as persistent store.
To read data, I use (only essential parts are shown):
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
persistentContainer.performBackgroundTask { (backgroundManagedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
var shoppingItems: Set<ShoppingItem> = []
do {
let cdShoppingItems: [CDShoppingItem] = try backgroundManagedContext.fetch(fetchRequest)
for nextCdShoppingItem in cdShoppingItems {
nextCdShoppingItem.managedObjectContext!.performAndWait {
Thread.sleep(forTimeInterval: 0.1) // This seems to be required
let nextShoppingItem = ShoppingItem.init(name: nextCdShoppingItem.name!)
shoppingItems.insert(nextShoppingItem)
} // performAndWait
} // for all cdShoppingItems
completion(shoppingItems, nil)
return
} catch let error as NSError {
completion(nil, error)
return
} // fetch error
} // performBackgroundTask
} // fetchShoppingItems
To test the coreData implementation, I wrote a unit test that creates multiple threads that write to and read from coreData concurrently.
This test runs only successfully, if the instruction
Thread.sleep(forTimeInterval: 0.1) // This seems to be required
is inserted in the performAndWait closure.
If it is commented out, nextCdShoppingItem is often read back with nil attributes, and the function crashes due to the forced unwrap.
I am not sure, if nextCdShoppingItem.managedObjectContext!.performAndWait is correct or if I had to use backgroundManagedContext.performAndWait, but with backgroundManagedContext the effect is the same.
I do not understand why inserting a small delay before accessing an attribute of a managed object is necessary to avoid the problem.
Any hint is welcome!
EDIT:
I investigated the issue further, and found the following:
Every time nextCdShoppingItem is read back by the background thread (called read thread below) as nil, there is also another background thread that tries to save its own managedContext after all records in its managedContext have been deleted (called write thread below).
Apparently the read thread tries to fetch a record that has just been deleted by the write thread.
So the problem is definitively a multithreading issue, and I found a solution (see my answer below).
performAndWait will add the block to the queue and schedule it to run, just like perform, but performAndWait will not return until the block is complete. Since you are inside a loop of cdShoppingItems, the loop does not stop and wait for the block to return. By adding the thread sleep, you are essentially slowing down the loop and giving core data enough time to complete its fetch. The forced unwrap crash is probably an indication that it's lost its nextCdShoppingItem reference.
I would consider refactoring where you do not need to query core data inside a loop. If it's possible, add the name attribute to CDShoppingItem so you don't have to fetch it to build a ShoppingItem object.
Edit: took a stab at a refactor although I don't know your exact use case:
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
persistentContainer.performBackgroundTask { (backgroundManagedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
var shoppingItems: Set<ShoppingItem> = []
let cdShoppingItems: [CDShoppingItem] = try backgroundManagedContext.fetch(fetchRequest)
for nextCdShoppingItem in cdShoppingItems {
if let name = nextCdShoppingItem.name {
let nextShoppingItem = ShoppingItem.init(name: name)
shoppingItems.insert(nextShoppingItem)
}
}
completion(shoppingItems, nil)
} catch let error as NSError {
print("Error fetching CDShoppingItem: \(error)")
completion(nil, error)
} // fetch error
return
} // performBackgroundTask
} // fetchShoppingItems
To prevent the multithreading issue, I tried 2 things:
1) Since iOS10, a persistentStore of SQL type maintains a connection pool for concurrent access to the pool, and it is possible to set a maximum pool size, see the WWDC video. I did so using
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// ...
} else {
storeDescription.setOption(NSNumber(1), forKey: NSPersistentStoreConnectionPoolMaxSizeKey)
}
})
return container
}()
to prevent concurrent access to the persistentStore. Unfortunately, this did not solve the problem for an unknown reason.
2) I then tried to serialize read and write operations by setting up a serial queue:
private let coreDataSerialQueue = DispatchQueue(label: "com.xxx.ShopEasy.coreDataManager") // serial by default
It is used for the read and write accesses in the following way:
coreDataSerialQueue.async {
let backgroundManagedContext = self.persistentContainer.newBackgroundContext()
backgroundManagedContext.performAndWait {
// …
} // performAndWait
} // coreDataSerialQueue.async
This did solve the problem.
Please note that it would be wrong to use
coreDataSerialQueue.async {
self.persistentContainer.performBackgroundTask { (backgroundManagedContext) in
// …
} // performBackgroundTask
} // coreDataSerialQueue.async
because performBackgroundTask would fork another asynchronous thread and thus break the serialization.
So according to Apple, I need to call the HKObserverQueryCompletionHandler after the updateHandler is triggered from data being added to HK.
But as soon as I call the completionHandler, the observer query stops giving any more updates in the background..
Here is my code:
guard let sampleType = sample as? HKQuantityType else { return nil }
let query = HKObserverQuery(sampleType: sampleType, predicate: nil, updateHandler: { query, completionHandler, error in
completionHandler()
IamExecutingHKStatisticsCollectionQueryHere()
})
healthStore?.execute(query)
healthStore?.enableBackgroundDelivery(for: sampleType, frequency: .hourly, withCompletion: { success, error in
})
If I don't call the completionHandler, everything works fine but I have never tested for long periods of time..
Calling completionHandler() indicates that you are done processing new data. Only call it once you have processed the results of the queries you execute in response to updateHandler being called. If you call completionHandler() early as you are now, the system will stop running your app in the background before you have a chance to process data.
I have an application that need to track user heart rate readings from apple watch, so I did all the required steps that I found on apple guides, and here is the code that I am using:
static var query: HKObserverQuery?
func startObservingHeartRate() {
guard let heartRateSampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate) else {
fatalError("Unable to create a step count sample type")
}
AppDelegate.query = HKObserverQuery(sampleType: heartRateSampleType, predicate: nil, updateHandler: { (query, completionHandler, error) in
if error != nil {
// Perform Proper Error Handling Here...
print("An error occured while setting up the Heart Rate observer.")
}
//Read the last strored heatt rate in add it to the DB
//Add last fetched Heart Rate reading to DB and send it to clips
HealthKitManager().fetchLastStoredHeartRate(completion: { (lastReading, error) in
guard let lastReading = lastReading else {
//There is no heart readings in HealthKit
return
}
//Check if Last HR value is Abnormal
if lastReading.doubleValue > 60 {
//TODO: - Schedule notification
if UIApplication.shared.applicationState == .background {
} else {
//TODO: - Show popup to the user
}
}
})
completionHandler()
})
healthKitStore.execute(AppDelegate.query!)
configureHeartRateObserver()
}
func configureHeartRateObserver() {
guard let heartRateSampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate) else {
fatalError("Unable to create a step count sample type")
}
healthKitStore.enableBackgroundDelivery(for: heartRateSampleType, frequency: HKUpdateFrequency.immediate) { (success, error) in
if success {
print("Enabled background delivery of Heart Rate changes")
} else {
print("Failed to enable background delivery of weight changes. ")
}
}
}
and I am calling "startObservingHeartRate" in didFinishLaunchingWithOptions in AppDelegate, assuming that this query should be executed once a new reading added or deleted from the health kit store, every thing is fine, if app is in background or killed the handler wake up my app and it do the updates.
But whenever I put the app in background then put it in foreground again it execute the observer query for many times even if there is no new readings added to the HealthKit store and in this case I am getting the same last heart rate for many times for no reason.
Please any recommendation on how to use this types of query or any changes I need to do with my current implementation.
If you want to track added and removed heart rate samples more precisely, you should use an HKAnchoredObjectQuery. HKObserverQuery does not guarantee that its update handler will only be called when a sample is added or removed. Note that you must continue executing an HKObserverQuery in addition to HKAnchoredObjectQuery since you are also using enableBackgroundDelivery(for:frequency:completion:).
I'm trying to make my app synchronize HealthKit data with our database whenever new data input is observed. Despite some ambiguity in the logic I believe I've managed to enable background delivery for the sample types and have the observers react when needed.
However, in the observer's updatehandler I need to create an HKAnchoredObjectQuery for fetching the most recent results from HealthKit, but these queries don't return reliably when my app is running in the background. Often when I add sample points in HealthKit, the anchored queries just get executed, but they return only when I bring the app back in the foreground.
Other times they return immediately without me having to activate the app. One possible issue is that all of my observer queries fire their updatehandlers, thus creating multiple AnchoredObjectQueries that may or may not return. I've explained that further in the linked thread, because it may be unrelated.
Here's an example of the function I'm calling from HKObserverQuery's updatehandler:
func synchronizeRecentData(sampleType: HKSampleType, observerQuery: HKObserverQuery) {
let completionHandler: (HKAnchoredObjectQuery, [HKSample]?, Int, NSError?) -> Void = {
[unowned self] query, results, newAnchor, error in
if error != nil {
abort()
}
// Update queryAnchor
self.queryAnchors[sampleType] = newAnchor
guard let receivedNewSamples = results as? [HKQuantitySample] else {
abort()
}
// Handle received samples here
}
let query = HKAnchoredObjectQuery(type: sampleType,
predicate: nil,
anchor: self.queryAnchors[sampleType]!,
limit: HKObjectQueryNoLimit,
completionHandler: completionHandler)
healthKitStore.executeQuery(query)
}
According to the HealthKit docs:
Unlike the observer query, these updates include a list of items that
have been added or removed; however, anchored object queries cannot be
registered for background delivery. For more information, see
HKAnchoredObjectQuery.
The only query that can register for background delivery is the HKObserverQuery
I believe this is because when your app gets woken up in the background you have very limited time to execute before the app gets suspended again. If you use UIBackgroundTaskIdentifier you should be able to ensure that your app runs long enough to finish getting the results of the HKAnchoredObjectQuery.