HKObserverQuery only runs when the application is reopened - ios

So I've been following the instructions in this answer...
Healthkit background delivery when app is not running
The code runs fine and works whilst the application is open and says that background delivery is successful, however when I test the application by walking around and changing the clock on the device to an hour forward I do not receive any logs to let me know it has run. However, if I open the application again the observer query runs.
private func checkAuthorization(){
let healthDataToRead = Set(arrayLiteral: self.distanceQuantityType!)
healthKitStore.requestAuthorization(toShare: nil, read: healthDataToRead) { (success, error) in
if error != nil {
print(error?.localizedDescription)
print("There was an error requesting Authorization to use Health App")
}
if success {
print("success")
}
}
}
public func enableBackgroundDelivery() {
self.checkAuthorization()
self.healthKitStore.enableBackgroundDelivery(for: self.distanceQuantityType!, frequency: .hourly) { (success, error) in
if success{
print("Background delivery of steps. Success = \(success)")
}
if let error = error {
print("Background delivery of steps failed = \(error.localizedDescription)")
}
}
}
func observeDistance(_ handler:#escaping (_ distance: Double) -> Void) {
let updateHandler: (HKObserverQuery?, HKObserverQueryCompletionHandler?, Error?) -> Void = { query, completion, error in
if !(error != nil) {
print("got an update")
completion!()
} else {
print("observer query returned error: \(error)")
}
}
let query = HKObserverQuery(sampleType: self.distanceQuantityType!, predicate: nil, updateHandler: updateHandler)
self.healthKitStore.execute(query)
}
The query is initialised in the appDelegate method didFinishLaunching

This particular HealthKitQuery is asynchronous. You should wait until it finishes processing.
However, this case is not possible in didFinishLaunching. The application just ended execution and there is not enough time to process the query.
I would seriously suggest to rethink the logic behind the operation of your code. A good way to solve this would be to put the request elsewhere, preferrably after the needed operations were completed.

Related

enableBackgroundDelivery Health-kit iOS15 not working correctly

I am working on the health kit. Before iOS15 below code is working correctly I am getting vitals on an hourly basis but in this latest version, I am not getting any vitals event per hour.
I am facing an issue in iOS15 not getting any event in an hourly event. Can anyone help me out? As per apple documentation https://developer.apple.com/documentation/healthkit/hkhealthstore/1614175-enablebackgrounddelivery I have enabled the permission in XCode13 still I am facing this issue.
Here is my code. (Working code before iOS15)
private func startObservingStepCountChanges() {
let sampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)
let query: HKObserverQuery = HKObserverQuery(sampleType: sampleType!, predicate: nil, updateHandler: self.stepChangeHandler)
healthKitStore.execute(query)
healthKitStore.enableBackgroundDelivery(for: sampleType!, frequency: .hourly, withCompletion: {(succeeded: Bool, error: Error!) in
if succeeded{
print("Enabled background delivery of stepcount changes")
} else {
if let theError = error{
print("Failed to enable background delivery of stepcount changes. ")
print("Error = \(theError)")
}
}
} as (Bool, Error?) -> Void)
}
private func stepChangeHandler(query: HKObserverQuery!, completionHandler: HKObserverQueryCompletionHandler!, error: Error!) {
// Flag to check the background handler is working or not
print("Backgound Mode activated")
fireTestPush()
completionHandler()
}
Check if background app refresh is enabled for your app. If it is and you're still running into this issue, file a feedback assistant report to Apple with sample code and sysdiagnose.

CKQueryOperation queryCompletionBlock not called

I'm using a CKQueryOperation that apparently works fine for most of my users. However, for some of them it is not working. The issue is that queryCompletionBlock is not being called.
Analysing user logs I can see that it works fine for most of the users, but it doesn't work for a few of them. It fails in all kind of iPhone models. But iOS is always iOS 14.2 on the failing devices. Unfortunately, I can not reproduce the issue on my device and this makes impossible to debug it.
I've already checked that the issue is not related with the internet connection type (wifi or data)
Any idea?
This is the code
func fetchTeams(_ success: #escaping (_ result: [CKRecord]?) -> Void,
failure: #escaping (_ error: NSError) -> Void) {
bfprint("fetchTeams starts")
let type = RecordType.Team
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: type.rawValue, predicate: predicate)
let operation = CKQueryOperation(query: query)
allTeamsRecords = []
executeQueryOperation(operation,
success: success,
failure: failure)
}
private func executeQueryOperation(_ queryOperation: CKQueryOperation,
success: #escaping (_ result: [CKRecord]?) -> Void,
failure: #escaping (_ error: NSError) -> Void) {
bfprint("executeQueryOperation starts")
let configuration = CKOperation.Configuration()
configuration.qualityOfService = .userInitiated
queryOperation.configuration = configuration
queryOperation.queuePriority = .veryHigh
queryOperation.recordFetchedBlock = { [weak self] (record) in
guard let strongSelf = self else {
bfprint("CloudKitDataProvider was deallocated before we got all team records")
return
}
strongSelf.allTeamsRecords.append(record)
}
queryOperation.queryCompletionBlock = { [weak self] (cursor, error) in
bfprint("fetch teams operation completion block called")
if let cursor = cursor {
bfprint("We got a new cursor fetching teams")
let newOperation = CKQueryOperation(cursor: cursor)
guard let strongSelf = self else {
bfprint("CloudKitDataProvider was deallocated before we got all team records")
return
}
strongSelf.executeQueryOperation(newOperation,
success: success,
failure: failure)
}
else if let error = error {
DispatchQueue.main.async(execute: {
failure(error as NSError)
bfprint("Cloud Query Error - Fetching Teams): \(error)")
})
}
else {
DispatchQueue.main.async(execute: {
bfprint("Get teams finished successfully")
guard let strongSelf = self else {
bfprint("CloudKitDataProvider was deallocated before we execute success closure")
return
}
success(strongSelf.allTeamsRecords)
})
}
}
Self.publicDB.add(queryOperation)
bfprint("query added to database")
}
I don't know what's specifically wrong in your situation, but I might offer some general guidance with CloudKit as I've worked with it over the years. CloudKit is really reliable, but it's also a little unreliable. :)
Here are some tips:
Build in mechanisms to repeatedly check that you have the latest data.
Background notifications don't always come. Have a way to get data that may have been missed.
Development and production behave a little differently in that dev seems to be a bit less reliable overall.
The CloudKit Dashboard needs to be refreshed (like the whole page in your web browser) from time-to-time because the state shown can get stale even when using the reload and query buttons in the interface.
So in your case, you might have a way to repeatedly try the CKQueryOperation so that it gets reattempted if something is haywire on CloudKit. Maintaining a local cache that syncs with CloudKit is the best way I've found to make sure your data is accurate.
I'm 99% sure it was an iOS issue. After users updated to iOS 14.3 the problem disappeared

How to query for HealthKit (HKWorkout) updates when app is in background?

EDIT to add my updated code which I based on WWDC 2016's Getting the Most Out of Healthkit talk, but I still am not getting my print statement with the new workout to fire unless I open the app?
I'm trying to observe for new workouts on the iPhone after they've been saved on the Apple Watch. Below is the code I'm running in didFinishLaunching. To test it, I'm running Xcode on my iPhone App...building and running, then navigating back to the home screen. Then starting and saving a workout on my watch, however my print statements aren't printing in the console. What am I missing?
func startObservingNewWorkouts() {
let sampleType = HKObjectType.workoutType()
//1. Enable background delivery for workouts
self.healthStore.enableBackgroundDelivery(for: sampleType, frequency: .immediate) { (success, error) in
if let unwrappedError = error {
print("could not enable background delivery: \(unwrappedError)")
}
if success {
print("background delivery enabled")
}
}
//2. open observer query
let query = HKObserverQuery(sampleType: sampleType, predicate: nil) { (query, completionHandler, error) in
self.updateWorkouts() {
completionHandler()
}
}
healthStore.execute(query)
}
func updateWorkouts(completionHandler: #escaping () -> Void) {
var anchor: HKQueryAnchor?
let sampleType = HKObjectType.workoutType()
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: anchor, limit: HKObjectQueryNoLimit) { [unowned self] query, newSamples, deletedSamples, newAnchor, error in
self.handleNewWorkouts(new: newSamples!, deleted: deletedSamples!)
anchor = newAnchor
completionHandler()
}
healthStore.execute(anchoredQuery)
}
func handleNewWorkouts(new: [HKSample], deleted: [HKDeletedObject]) {
print("new sample added = \(new.last.startTime!)")
}
Turns out this code 👆 works, its just that I was testing in the simulator and apparently the Observer Query does NOT fire when running in the simulator but it DOES fire when running on device

Firebase observer architecture

Okay, so I'm trying to build an iOS app that relies on Firebase (To work with its android version)
I started with creating a repository for each actor in my app and a general repository to manage them all
Each repository manages the observers of this actor. An example:
Inside the PagesRepository, this is a function that retrieves all the pages from Firebase and returns it inside a completionHandler:
//MARK: Gets the whole pages list
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observe(DataEventType.value) { pagesSnapshot in
guard pagesSnapshot.exists() else {
displayError(error: "Pages snapshot doesn't exist")
return
}
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}
}
And then I call it from the ViewController like this:
repository.getPagesList { (pages, error) in
guard error == nil else {
return
}
//Do processing
}
I know this may be a lot to take in, but my problem is that every time I call the function, it creates a new observer but doesn't cancel the old one... So, the completionHandler is called multiple times with different values
How should I manage this problem?
(Sorry for being complicated and a little unclear, I'm just really lost)
It seems like you only want to observe the value once so I would use this instead:
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observeSingleEvent(of: .value, with: { (pagesSnapshot) in
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}) { (error) in
// Display error
}
}

HealthKit Observer not working while app is in background mode

I have followed Apple Docs and Several threads on stackoverflow on how to achieve background fetching of data from Health Store.
So far I have:
Added HealthKit Entitlement to my appID
Added Required Background Modes
Added the code to AppDelegate.swift as Apple suggest (the snippet below is not following OOP just for facility to state this way here)
This is my code (swift):
If your answer is in Obj-C and works, please state it as well, I will have to translate it, but that's no problem.
AppDelegate.swift
var healthStore: HKHealthStore?
var bpmSamples: [HKQuantitySample]?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let dataTypesToWrite = [ ]
let dataTypesToRead = [
HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate),
HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMassIndex),
HKCharacteristicType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierDateOfBirth)
]
if self.healthStore == nil {
self.healthStore = HKHealthStore()
}
self.healthStore?.requestAuthorizationToShareTypes(NSSet(array: dataTypesToWrite as [AnyObject]) as Set<NSObject>,
readTypes: NSSet(array: dataTypesToRead) as Set<NSObject>, completion: {
(success, error) in
if success {
self.addQueryObserver()
println("User completed authorisation request.")
} else {
println("The user cancelled the authorisation request. \(error)")
}
})
return true
}
func addQueryObserver(){
let sampleType =
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let query = HKObserverQuery(sampleType: sampleType, predicate: nil) {
query, completionHandler, error in
if error != nil {
// Perform Proper Error Handling Here...
println("*** An error occured while setting up the stepCount observer. \(error.localizedDescription) ***")
abort()
}
println("query is running")
self.performQueryForHeartBeatSamples()
completionHandler()
}
healthStore?.executeQuery(query)
healthStore?.enableBackgroundDeliveryForType(sampleType, frequency:.Immediate, withCompletion:{
(success:Bool, error:NSError!) -> Void in
let authorized = self.healthStore!.authorizationStatusForType(sampleType)
println("HEALTH callback success", success)
println("HEALTH callback authorized", sampleType)
})
if HKHealthStore.isHealthDataAvailable() == false {
println("HEALTH data not available")
return
} else {
println("HEALTH OK")
self.performQueryForHeartBeatSamples()
}
}
// MARK: - HealthStore utility methods
func performQueryForHeartBeatSamples() {
let endDate = NSDate()
let startDate = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitMonth, value: -2, toDate: endDate, options: nil)
var heartRate : HKQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: .None)
let query = HKSampleQuery(sampleType: heartRate, predicate: predicate, limit: 0, sortDescriptors: nil, resultsHandler: {
(query, results, error) in
if results == nil {
println("There was an error running the query: \(error)")
}
dispatch_async(dispatch_get_main_queue()) {
self.bpmSamples = results as? [HKQuantitySample]
let heartRateUnit: HKUnit = HKUnit.countUnit().unitDividedByUnit(HKUnit.minuteUnit())
if self.bpmSamples?.count > 0 {
if let sample = self.bpmSamples?[self.bpmSamples!.count - 1] {
println(sample.quantity!.description)
let quantity = sample.quantity
var value = quantity.doubleValueForUnit(heartRateUnit)
println("bpm: \(value)")
}
}
else {
println("No Data")
}
}
})
self.healthStore?.executeQuery(query)
}
So, the problem is that I only receive updates when I resume my app from background to active state manually.. HKObserverQuery doesn't seems to be working for me while on background mode.
Any suggestions?
In my experiences, frequency ".Immediate" doesn't work well. The handler of queries are inserted into background queue. If the matching samples are frequently added or iOS is busy, the immediate frequency doesn't work well.
In addition, you cannot use HKSampleQuery in HKObserverQuery. updateHandler of HKObserverQuery may work, but resultHandler of HKSampleQuery will not. The handler of an observer query can be executed in the background mode but the one of a sample query cannot be executed in the background mode.
You should know that ONLY HKObserverQuery can be used in the background

Resources