HealthKit: HKObserverQuery not Firing - ios

I'm trying to do a pretty basic thing: set up an HKObserverQuery so I can know when various data points are changed (I've made sure that the user has authorized the app to use the data point in question.) For whatever reason, I can get the query to fire every time the app is launched, but it does not fire when I close the app, go into the Health app, and manually update the data point. I've done a fair amount of searching and haven't been able to successfully use the code that others have posted, code that they say works for them.
I'm two weeks into Cocoa/Objective C development, so I'm sure I'm missing something obvious, but I can't see what it is. Any guidance here would be great, even if it's just advice on debugging. Since the app itself is closed and I'm not getting anything that it might log out in the console, I don't really have any visibility into what's happening.
The code that I'm using for the observer query is below:
HKQuantityType *heartRate = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeartRate];
[self.healthStore enableBackgroundDeliveryForType:heartRate frequency:HKUpdateFrequencyImmediate withCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"observing heart rate");
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://www.bodbot.com/Scripts/wearable_heartrate_changed.php"]];
}else{
NSLog(#"FAILED observing heart rate");
}
}];
HKObserverQuery *query = [[HKObserverQuery alloc] initWithSampleType:heartRate predicate:nil updateHandler:^(HKObserverQuery *query, HKObserverQueryCompletionHandler completionHandler, NSError *error) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://www.bodbot.com/Scripts/wearable_heartrate_changed.php"]];
}];
[self.healthStore executeQuery:query];
Thanks!

I have found, empirically (not from documentation), that the Observer Query does NOT fire when running in the simulator but it DOES fire when running on device. And I do not have the background modes capability turned on.

When you suspend an app on iOS, by default it stops running unless it has taken a background task assertion or has a background mode entitlement. The app cannot receive notifications when it is not running. HealthKit has a feature which can wake your app in the background when there are new samples of a particular type, though. See the Managing Background Delivery documentation for HKHealthStore. Use that in conjunction with HKObserverQuery to be notified whenever there is new data, even when your app is not already running.

Related

iPad Pro 3rd Gen Killing Foreground App Without Cause

I have an app that has been out in the wild for many years.
This app, in order to be 100% functional while offline, needs to download hundreds of thousands of images (1 for each object) one time only (delta updates are processed as needed).
The object data itself comes down without issue.
However, recently, our app has started crashing while downloading just the images, but only on newer iPads (3rd gen iPad Pros with plenty of storage).
The image download process uses NSURLSession download tasks inside an NSOperationQueue.
We were starting to see Energy Logs stating that CPU usage was too high, so we modified our parameters to add a break between each image, as well as between each batch of image, using
[NSThread sleepForTimeInterval:someTime];
This reduced our CPU usage from well above 95% (which, fair enough) to down below 18%!
Unfortunately, the app would still crash on newer iPads after only a couple of hours. However, on our 2016 iPad Pro 1st Gen, the app does not crash at all, even after 24 hours of downloading.
When pulling crash logs from the devices, all we see is that CPU usage was over 50% for more than 3 minutes. No other crash logs come up.
These devices are all plugged in to power, and have their lock time set to never in order to allow the iPad to remain awake and with our app in the foreground.
In an effort to solve this issue, we turned our performance way down, basically waiting 30 seconds in between each image, and 2 full minutes between each batch of images. This worked and the crashing stopped, however, this would take days to download all of our images.
We are trying to find a happy medium where the performance is reasonable, and the app does not crash.
However, what is haunting me, is that no matter the setting, and even at full-bore performance, the app never crashes on the older devices, it only crashes on the newer devices.
Conventional wisdom would suggest that should not be possible.
What am I missing here?
When I profile using Instruments, I see the app sitting at a comfortable 13% average while downloading, and there is a 20 second gap in between batches, so the iPad should have plenty of time to do any cleanup.
Anyone have any ideas? Feel free to request additional information, I'm not sure what else would be helpful.
EDIT 1: Downloader Code Below:
//Assume the following instance variables are set up:
self.operationQueue = NSOperationQueue to download the images.
self.urlSession = NSURLSession with ephemeralSessionConfiguration, 60 second timeoutIntervalForRequest
self.conditions = NSMutableArray to house the NSConditions used below.
self.countRemaining = NSUInteger which keeps track of how many images are left to be downloaded.
//Starts the downloading process by setting up the variables needed for downloading.
-(void)startDownloading
{
//If the operation queue doesn't exist, re-create it here.
if(!self.operationQueue)
{
self.operationQueue = [[NSOperationQueue alloc] init];
[self.operationQueue addObserver:self forKeyPath:KEY_PATH options:0 context:nil];
[self.operationQueue setName:QUEUE_NAME];
[self.operationQueue setMaxConcurrentOperationCount:2];
}
//If the session is nil, re-create it here.
if(!self.urlSession)
{
self.urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]
delegate:self
delegateQueue:nil];
}
if([self.countRemaining count] == 0)
{
[self performSelectorInBackground:#selector(startDownloadForNextBatch:) withObject:nil];
self.countRemaining = 1;
}
}
//Starts each batch. Called again on observance of the operation queue's task count being 0.
-(void)startDownloadForNextBatch:
{
[NSThread sleepForTimeInterval:20.0]; // 20 second gap between batches
self.countRemaining = //Go get the count remaining from the database.
if (countRemaining > 0)
{
NSArray *imageRecordsToDownload = //Go get the next batch of URLs for the images to download from the database.
[imageRecordsToDownload enumerateObjectsUsingBlock:^(NSDictionary *imageRecord,
NSUInteger index,
BOOL *stop)
{
NSInvocationOperation *invokeOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(downloadImageForRecord:)
object:imageRecord];
[self.operationQueue addOperation:invokeOp];
}];
}
}
//Performs one image download.
-(void)downloadImageForRecord:(NSDictionary *)imageRecord
{
NSCondition downloadCondition = [[NSCondition alloc] init];
[self.conditions addObject:downloadCondition];
[[self.urlSession downloadTaskWithURL:imageURL
completionHandler:^(NSURL *location,
NSURLResponse *response,
NSError *error)
{
if(error)
{
//Record error below.
}
else
{
//Move the downloaded image to the correct directory.
NSError *moveError;
[[NSFileManager defaultManager] moveItemAtURL:location toURL:finalURL error:&moveError];
//Create a thumbnail version of the image for use in a search grid.
}
//Record the final outcome for this record by updating the database with either an error code, or the file path to where the image was saved.
//Sleep for some time to allow the CPU to rest.
[NSThread sleepForTimeInterval:0.05]; // 0.05 second gap between images.
//Finally, signal our condition.
[downloadCondition signal];
}]
resume];
[downloadCondition lock];
[downloadCondition wait];
[downloadCondition unlock];
}
//If the downloads need to be stopped, for whatever reason (i.e. the user logs out), this function is called to stop the process entirely:
-(void)stopDownloading
{
//Immediately suspend the queue.
[self.operationQueue setSuspended:YES];
//If any conditions remain, signal them, then remove them. This was added to avoid deadlock issues with the user logging out and then logging back in in rapid succession.
[self.conditions enumerateObjectsUsingBlock:^(NSCondition *condition,
NSUInteger idx,
BOOL * _Nonnull stop)
{
[condition signal];
}];
[self setConditions:nil];
[self setConditions:[NSMutableArray array]];
[self.urlSession invalidateAndCancel];
[self setImagesRemaining:0];
[self.operationQueue cancelAllOperations];
[self setOperationQueue:nil];
}
EDIT 2: CPU usage screenshot from Instruments. Peaks are ~50%, valleys are ~13% CPU usage.
EDIT 3: Running the app until failure in Console, observed memory issue
Alright! Finally observed the crash on my iPhone 11 Pro after over an hour downloading images, which matches the scenario reported by my other testers.
The Console reports my app was killed specifically for using too much memory. If I am reading this report correctly, my apps used over 2 GB of RAM. I'm assuming that this has to do more with the internal management of NSURLSESSIOND, since it is not showing this leak during debugging with either Xcode or Instruments.
Console reports: "kernel 232912.788 memorystatus: killing_specific_process pid 7075 [PharosSales] (per-process-limit 10) 2148353KB - memorystatus_available_pages: 38718"
Thankfully, I start receiving memory warnings around the 1 hour mark. I should be able to pause (suspend) my operation queue for some time (let's say 30 seconds) in order to let the system clear its memory.
Alternatively, I could call stop, with a gcd dispatch after call to start again.
What do you guys think about this solution? Is there a more elegant way to respond to memory warnings?
Where do you think this memory usage is coming from?
EDIT 4: Eureka!! Found internal Apple API memory leak
After digging I 'killing specific process' memory-related console message, I found the following post:
Stack Overflow NSData leak discussion
Based on this discussion surrounding using NSData writeToFile:error:, I looked around to see if I was somehow using this function.
Turns out, the logic that I was using to generate a thumbnail from the original image used this statement to write the generated thumbnail image to disk.
If I commented out this logic, the app no longer crashed at all (was able to pull down all of the images without failure!).
I had already planned on swapping this legacy Core Graphics code out for the WWDC 2018-demonstrated usage of ImageIO.
After recoding this function to use ImageIO, I am pleased to report that the app no longer crashes, and the thumbnail logic is super-optimized as well!
Thanks for all your help!

iOS Cloudkit fetchRecordWithID is not returning a response

This case is happening on only one particular test device (xs-max). The other devices we could not replicate this.
The completion block for fetchRecordWithID isn't executing no matter how long I wait.
Here is the code I call on tap of button.
//recordID is not nil
[[[CKContainer defaultContainer] privateCloudDatabase]fetchRecordWithID:recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if (record) {
//save record
}else{
NSLog(#"%#",error);
}
}];
I couldn't debug this as the execution never reaches the block.
Any case this might happen ?
I am not sure if this is the case, but the device iCloud settings are not loaded properly.
We had to restart the device which prompted to re-validate the apple ID in the settings.
Once the validation is done, the icloud settings are shown as expected. And there after, the code executed as expected.
Adding screenshot for reference.

Does the AppDelegate initialize when Healthkit wakes my app in the background?

I use the code below to have my app detect updates to HealthKit data in the background. Will the init method of my AppDelegate be called when this code is run in the background? What methods in the AppDelegate will be called? If someone can provide documentation about the application lifecycle of background code, that will be much appreciated!
[healthStore enableBackgroundDeliveryForType:type frequency:HKUpdateFrequencyHourly withCompletion:^(BOOL success, NSError *error) {
if (success) {
HKObserverQuery *observerQuery = [[HKObserverQuery alloc] initWithSampleType:type
predicate:nil
updateHandler:^(HKObserverQuery *query, HKObserverQueryCompletionHandler completionHandler, NSError *error) {
if (!error) {
[self retrieveHealthDataWithCompletionHandler:completionHandler];
}
}];
[healthStore executeQuery:observerQuery];
}
A bit late but hopefully, it would still help you or anyone else who reach here..
When your app delegate’s application:didFinishLaunchingWithOptions: method is being called you can assume the app launches. Thats why Apple recommend you'll register any observer queries you'd like to have inside that method.
When there will be new data of the type you registered for, HealthKit will wake your app. (So far you still don't know anything about any new data.) Once your app finish its launching it will call the beloved app delegate’s application:didFinishLaunchingWithOptions: method, which as stated before, should contain the code of registering the observer queries.
Once you register your queries, next thing will be getting an update about new data (this is the purpose of observer queries).
Getting the update about something new in HealthKit doesn't contain the data itself. Thats why in the updateHandler of the observer query you should initiate anther query - a more specific one that will fetch the wanted data.
That's it. I would make some changes in the code you provided in order for it to work:
[healthStore enableBackgroundDeliveryForType:type frequency:HKUpdateFrequencyHourly withCompletion:^(BOOL success, NSError *error) {
if (success) {
//Nothing much to do here
}
}];
HKObserverQuery *observerQuery = [[HKObserverQuery alloc] initWithSampleType:type
predicate:nil
updateHandler:^(HKObserverQuery *query, HKObserverQueryCompletionHandler completionHandler, NSError *error) {
if (!error) {
//Create and execute a query about the sample type.
// Within the completion handler of the new query, don't forget to call completionHandler()
}
}];
[healthStore executeQuery:observerQuery];
You can find more details here:
Receiving Background Deliveries
Apps can also register to receive updates while in the background by calling the HealthKit store’s enableBackgroundDeliveryForType:frequency:withCompletion: method. This method registers your app for background notifications. HealthKit wakes your app whenever new samples of the specified type are saved to the store. Your app is called at most once per time period defined by the frequency you specified when registering.
As soon as your app launches, HealthKit calls the update handler for any observer queries that match the newly saved data. If you plan on supporting background delivery, set up all your observer queries in your app delegate’s application:didFinishLaunchingWithOptions: method. By setting up the queries in application:didFinishLaunchingWithOptions:, you ensure that the queries are instantiated and ready to use before HealthKit delivers the updates.
After your observer queries have finished processing the new data, you must call the update’s completion handler. This lets HealthKit know that you have successfully received the background delivery.

Can Firebase send and receive in the background on iOS 7?

My Objective C app on iOS 7 gets location updates in the background from either the startUpdatingsignificantLocationChanges or startUpdatingLocation delegate (which one depends on the mode that the app is in, but I don't think it matters).
In the delegate, I gather the location info, write it to a dictionary, and then write the dictionary to a Firebase.
// this code is in the location update delegate routine
// the code that gathers the various elements that go into the dictionary
// are omitted for clarity, I don't think that they matter
// we may be running in the background on iOS 7 when we are called!
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
[[NSNumber numberWithFloat:newLocation.coordinate.latitude] stringValue], #"Latitude",
[[NSNumber numberWithFloat:newLocation.coordinate.longitude] stringValue], #"Longitude",
[[NSNumber numberWithFloat:newLocation.horizontalAccuracy] stringValue], #"Accuracy",
formattedDateString, #"TimeNow",
[dateFormatter stringFromDate:newLocation.timestamp], #"TimeStamp",
[[NSNumber numberWithDouble:interval] stringValue], #"Date",
self.mode, #"Mode",
nil];
// Write it once to CurrentLocation
[ref setValue:dictionary];
// yes, I know this is clumsy
fbTmp = [NSMutableString stringWithString: fbRoot];
[fbTmp appendString : #"/locationHistory"];
ref = [[Firebase alloc] initWithUrl:fbTmp];
// Now write it again to the locationHistory list
ref = [ref childByAutoId];
[ref setValue:dictionary];
Sometimes it works, sometimes it doesn't (i.e. in the same run of the app, sometimes the location gets written to the Firebase successfully as expected, and sometimes it doesn't. There isn't any obvious rhyme or reason to when it seems to work and when it doesn't).
I suspect that the issue is that the Firebase write is not completing successfully in background mode, but I'm not sure. I am very new to iOS and Objective C and Firebase.
My app is marked in its Capabilities as requiring background services for Location updates and Background fetch (the latter my random attempt to fix this problem, the former I know that I need).
My suspicion is that I need to tell the OS that I need time to complete the write with a backkgroundTask, and then terminate the background task in the completion block of the firebase write - has anyone verified that that will work when running in background mode?
If so, do I just need to do that in the second write (assuming that they are completed in order), or in both (with a counter that I count down as each write completes)?
Any hints most appreciated.
Yes, you need to finish your task in background. It says so in the Apple Documentation:
If your app is in the middle of a task and needs a little extra time to complete that task, it can call the beginBackgroundTaskWithName:expirationHandler: or beginBackgroundTaskWithExpirationHandler: method of the UIApplication object to request some additional execution time. Calling either of these methods delays the suspension of your app temporarily, giving it a little extra time to finish its work. Upon completion of that work, your app must call the endBackgroundTask: method to let the system know that it is finished and can be suspended.
Just put your code in a background task, give it maximum time (3 minutes I think).
Read up on Apple app lifecycle and everything should clear up for you for future reference.

Executing synchronous queries to Google Cloud Endpoints on iOS

I guess this is really a feature request to Google, but I'm curious if anyone knows a work around. I'd like to execute a synchronous query request to a GAE Endpoints api.
In Android executing a request is synchronous. Then you put it into an AsyncTask to make it work in the background.
In iOS executing a request is asynchronous. You simply pass in a callback block.
I'm converting an Android app into an iOS app and it'd be really nice if they used the same mechanism. For example there are times when I WANT a synchronous query. It just makes my code easier and I know to put it on a background thread.
So my question is this... is there any way (hacky or not) to block until the iOS query completes?
You can wait on the call to finish with code with a timeout using code similar to this. Obviously you wouldn't want to do this on a UI thread but this would ensure your completion handlers run in serial.
NSLog(#"Before API Call");
GTLServiceTicket *apiCall = [apiService executeQuery:query completionHandler:^(GTLServiceTicket *ticket,
GTLHelloworldHelloGreeting *object,
NSError *error) {
NSLog(#"Starting completion handler");
NSArray *greetings = [NSArray arrayWithObjects: object, nil];
greetingsRetrievedFromAPI = greetings;
[self performSegueWithIdentifier: #"DisplayGreetings" sender: self];
NSLog(#"Ending completion handler");
}];
[apiService waitForTicket:apiCall timeout:100000 fetchedObject:nil error:nil];
NSLog(#"After completion handler");

Resources