Healthkit background delivery when app is not running
Asked Answered
C

3

46

Can HealthKit background delivery launch the application if is not running? Particularly in a terminated state?

Camass answered 15/10, 2014 at 6:21 Comment(0)
B
81

After a full day of testing, I can confirm that HealthKit background delivery does work in all of the following application states:

  • background: in background and executing code,
  • suspended: in background and not executing code,
  • terminated: force-killed by the user or purged by the system.

Keep in mind: part 1

Some HealthKit data types have a minimum update frequency of HKUpdateFrequencyHourly. That said, even if you set up a background delivery with frequency HKUpdateFrequencyImmediate, you won't get updates more often than every hour or so.

Unfortunately, there is no info in the documentation about minimum frequencies per data type, but my experience with Fitness types was as follows:

  • Active Energy: hourly,
  • Cycling Distance: immediate,
  • Flights Climbed: immediate,
  • NikeFuel: immediate,
  • Steps: hourly,
  • Walking + Running Distance: hourly,
  • Workouts: immediate.

Note: immediate DOES NOT mean real-time but rather "some time shortly after" the activity data samples have been written to the HealthKit database/store.


Keep in mind: part 2

If the device is locked with a passcode, none of your background delivery observers will be called. This is intentional, due to privacy.

That said, as soon as a device is unlocked, your HealthKit background delivery observers will be called, given that the minimum frequency time has passed.


Sample code

Take a look at Viktor Sigler's answer, but you can skip all three steps from the beginning of his answer since they are not required for HealthKit background delivery to work.

Bandung answered 28/1, 2016 at 23:8 Comment(4)
I like the way you explained all the steps and deficiencies about it, I'll try to update the code to Swift 2.2 ASAP!!. I've been lazy to update it, but in the next days I'll do it to with your answer and my own the people can understand the process better. I think it could be great too co-create a tutorial about it in Medium to help more people, let me know what do you think?Nipper
In order to support background delivery when the app is terminated, one should set the observers in the AppDelegate's didFinishLaunchingWithOptions:. In the event of new data of the type you are observing, HealthKit will launch your app before it actually send the notification for the relevant observer. By setting the observer query in the didFinishLaunchingWithOptions: you are promised to be notify about any new data while your observers are ready. developer.apple.com/library/ios/documentation/HealthKit/…Laritalariviere
In the "terminated" state, are you able to upload the activity (steps, distance etc...) to a server? I find that my Observation handlers are being hit all the time, but it is hit or miss if I can upload the data to my server.Cityscape
@Goldengil Do you have some code you can share with us? Right now, all my HealthKit API calls, Observations etc... are running in a service called HealthData. My Observations and Observation handlers are working, but I don't always get the data uploaded to my server.Cityscape
N
40

This answer is some late but I hope this help the people to understand how to work with the HKObserverQuery successfully.

First of all the HKObserverQuery works fine in background mode and when the app is closed at all. But you need to set some options first to allow everything works fine.

  1. You need to set the Background Modes in the Capabilities of your app. See below picture:

enter image description here

  1. Then you need to add the Required Background Modes in your info.plist as in the following picture:

enter image description here

  1. You need to set the Background Fetch in the following way:

    3.1. From the Scheme toolbar menu, choose an iOS Simulator or Device.

    3.2. From the same menu, choose Edit Scheme.

    3.3. In the left column, select Run.

    3.4. Select the Options tab.

    3.5. Select the Background Fetch checkbox and click Close.

enter image description here

Then you can receive notifications when the app is in background or closed using the following code:

import UIKit
import HealthKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

   var window: UIWindow?

   let healthKitStore:HKHealthStore = HKHealthStore()

   func startObservingHeightChanges() {

       let sampleType =  HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)

       var query: HKObserverQuery = HKObserverQuery(sampleType: sampleType, predicate: nil, updateHandler: self.heightChangedHandler)

       healthKitStore.executeQuery(query)
       healthKitStore.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate, withCompletion: {(succeeded: Bool, error: NSError!) in

           if succeeded{
               println("Enabled background delivery of weight changes")
           } else {
               if let theError = error{
                   print("Failed to enable background delivery of weight changes. ")
                   println("Error = \(theError)")
               }
           }
       })
   }


   func heightChangedHandler(query: HKObserverQuery!, completionHandler: HKObserverQueryCompletionHandler!, error: NSError!) {        

       // Here you need to call a function to query the height change

       // Send the notification to the user
       var notification = UILocalNotification()
       notification.alertBody = "Changed height in Health App"
       notification.alertAction = "open"        
       notification.soundName = UILocalNotificationDefaultSoundName   

       UIApplication.sharedApplication().scheduleLocalNotification(notification)

       completionHandler()
   }

   func authorizeHealthKit(completion: ((success:Bool, error:NSError!) -> Void)!) {

       // 1. Set the types you want to read from HK Store
       let healthKitTypesToRead = [
        HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierDateOfBirth),
        HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBloodType),
        HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBiologicalSex),
        HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass),
        HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight),
        HKObjectType.workoutType()
       ]

       // 2. Set the types you want to write to HK Store
       let healthKitTypesToWrite = [
        HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMassIndex),
        HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned),
        HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning),
        HKQuantityType.workoutType()
       ]

       // 3. If the store is not available (for instance, iPad) return an error and don't go on.
       if !HKHealthStore.isHealthDataAvailable() {
           let error = NSError(domain: "any.domain.com", code: 2, userInfo: [NSLocalizedDescriptionKey:"HealthKit is not available in this Device"])

           if( completion != nil ) {                
               completion(success:false, error:error)
           }
           return;
       }

       // 4.  Request HealthKit authorization
       healthKitStore.requestAuthorizationToShareTypes(Set(healthKitTypesToWrite), readTypes: Set(healthKitTypesToRead)) { (success, error) -> Void in
           if( completion != nil ) {

               dispatch_async(dispatch_get_main_queue(), self.startObservingHeightChanges)
               completion(success:success,error:error)
           }
       }
   }   

   func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

       application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: .Alert | .Badge | .Sound, categories: nil))

       self.authorizeHealthKit { (authorized,  error) -> Void in
           if authorized {
               println("HealthKit authorization received.")
           }
           else {
               println("HealthKit authorization denied!")
               if error != nil {
                   println("\(error)")
               }
           }
       }

       return true
   }      


   //Rest of the defaults methods of AppDelegate.swift   

}

In the above method the HKObserver is activated if the HealthKit authorization is granted by the user and then activate notifications.

I hope this help you.

Nipper answered 1/6, 2015 at 15:32 Comment(16)
I don't think enabling Background Fetch is the right way to get changes in the Health store. Fetch launches the entire app in the background, including root view controller. This may not be the right approach. Please double check!Deming
I only post the way to make the observers works, at least for me, it's just an answer, it could be a better way as you said. I'll try to search...Nipper
It doesn't work for me. I've tried both on device and emulator. iOS 8.4. Does the notification should be delivered immediately after adding new data to Heal app?Nady
it works in iOS 8.1 but doesn't in iOS 8.4 It's an iOS bugNady
Yes, its works in iOS 8.4 but you need to omit the Step 3 , and it works, yes if you add new data to your Health App regarding the observer you set, the notification must be received.Nipper
I've omitted step 3 and it still don't work for me on iPhone 5 and on emulators as well :( Have you actually tested that it works?Nady
Yes of course, don't worry I going to upload a project to Github ASAP and I'll let you know.Nipper
@VictorSigler What do you mean by "Then you can receive notifications when the app is in background or closed"? What if the app is terminated aka force-killed by the user?Bandung
@Bandung Yes it's the same, the HKObserverQuery is running in a long background thread even when the app is force-killed by the userNipper
@Bandung Yes please give a couple of days and I'll upload a project sample I've prepared to GithubNipper
Yes I have the code working in a app that I working and work fine even when the app is not open. In the doc of the sample I'll try to update all the info I can about the way of make it workNipper
@VictorSigler Can you please give the link of the GitHub project. ThanksGeneralissimo
@MoazKhan It was a long time since I answered this question and I worked with HealthKit. Sorry but even if I find the code it's going to be very old. Nevertheless I'll try to find it :)Nipper
Did anyone get success in getting HealthKit notification by HKObserverQuery when the application is killed or closed or terminated? I am receiving notifications when the app is in the background but not when the application is killed.Overspill
@VictorSigler never gave the sample it seems and its 2022 lolEloquent
it's 2024 and on iOS17, I can confirm that background delivery is working.. I can get code to be executed when there's a new change in (I used steps count) using .immediate and then firing up more queries to get more data. This works in the when the app is in the foreground, in the background and even when terminated. Note that I didn't use notifications to trigger, I just triggered background code and also added logging statements to the filesystemLysenko
F
3

In iOS 8.1 it does. You need to make sure you recreate your observer queries in your app delegate's application:didFinishLaunchingWithOptions:, though. A bug in 8.0 prevents HealthKit's background notification from working at all.

EDIT:

In your AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //create/get your HKHealthStore instance (called healthStore here)
    //get permission to read the data types you need.
    //define type, frequency, and predicate (called type, frequency, and predicate here, appropriately)

    UIBackgroundTaskIdentifier __block taskID = [application beginBackgroundTaskWithExpirationHandler:^{
        if (taskID != UIBackgroundTaskInvalid) {
            [application endBackgroundTask:taskID];
            taskID = UIBackgroundTaskInvalid;
        }
    }];
    [healthStore enableBackgroundDeliveryForType:type frequency:frequency withCompletion:^(BOOL success, NSError *error) {}];
    HKQuery *query = [[HKObserverQuery alloc] initWithSampleType:healthType predicate:predicate updateHandler:
        ^void(HKObserverQuery *query, HKObserverQueryCompletionHandler completionHandler, NSError *error)
        {
            //If we don't call the completion handler right away, Apple gets mad. They'll try sending us the same notification here 3 times on a back-off algorithm.  The preferred method is we just call the completion handler.  Makes me wonder why they even HAVE a completionHandler if we're expected to just call it right away...
            if (completionHandler) {
                completionHandler();
            }
            //HANDLE DATA HERE
            if (taskID != UIBackgroundTaskInvalid) {
                [application endBackgroundTask:taskID];
                taskID = UIBackgroundTaskInvalid;
            }
        }];
    [healthStore executeQuery:query];
}
Farlie answered 15/10, 2014 at 14:38 Comment(4)
I haven't yet seen a working code example on iOS 8.1 as of yet. Can you refer one that will allow you to react to background updates?Transonic
Do the queries and observers and all that have to be in the auth completion block? If they're in the block, then didFinishLaunching will end before everything is set up, and the documentation says everything should be set up before this method completes.Voiture
It is not. All that needs to happen elsewhere or get handled differently when opening your app for the very first time.Farlie
Have you ever tested your code to receive heart rate data in real time from a watchOS2 workout and IOS9.1? The HKObserverQuery only fires at startup or if the IOS app comes from background to foreground? Is it a BUG?Jaguarundi

© 2022 - 2024 — McMap. All rights reserved.