Healthkit background delivery when app is not running

After a full day of testing (iOS 9.2) 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 but 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 documentation about minimum frequencies per data types, 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 has been written to the HealthKit database/store.

Keep in mind: part 2

If the device is locked with a passcode, none of you background delivery observers will be called. This is intentional due to the privacy concerns (read more: https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HealthKit_Framework/).

That said, as soon as the user unlocks the device, your HealthKit background delivery observers will be called (if the minimum frequency time has passed, of course).

Sample code:

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


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.


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];
}