当应用程序未运行时,HealthKit 后台传送的问题

44

HealthKit背景传递能否在应用程序未运行时启动应用程序?特别是在终止状态下?

3个回答

76
经过一整天的测试,我可以确认在以下所有应用程序状态下,HealthKit后台传递确实有效

  • 后台运行: 在后台并执行代码,
  • 挂起: 在后台而不执行代码,
  • 终止:被用户强制终止或被系统清除。

请记住:第1部分

某些HealthKit数据类型有最小更新频率HKUpdateFrequencyHourly。也就是说,即使您使用HKUpdateFrequencyImmediate设置了后台传递,您也不会比每小时或更长时间更新更快。

不幸的是,文档中没有关于每种数据类型的最小频率的信息,但我的Fitness类型经验如下:

  • 活跃能量:每小时
  • 骑行距离:立即
  • 攀登楼层:立即
  • NikeFuel:立即
  • 步数:每小时
  • 步行+跑步距离:每小时
  • 锻炼:立即

注意:立即并不意味着实时,而是“在活动数据样本被写入HealthKit数据库/存储后不久”。


请记住:第2部分

如果设备使用密码锁定,则没有一个后台传递观察者会被调用。这是出于隐私考虑而有意为之。

话虽如此,但只要设备解锁,您的HealthKit后台交付观察者将被调用,前提是已经过了最小频率时间。


示例代码

参考Viktor Sigler的答案,但是可以跳过他答案开头的三个步骤,因为这些步骤对于HealthKit后台交付无需求。


1
我喜欢你解释所有步骤和缺陷的方式,我会尽快将代码更新到Swift 2.2!我一直懒得更新它,但在接下来的几天里,我会结合你的答案和我的来更新,以便更好地让人们理解这个过程。我认为共同创作一个Medium教程也是很棒的想法,可以帮助更多的人,你觉得呢? - Victor Sigler
2
为了在应用程序终止时支持后台传送,应该在AppDelegate的 didFinishLaunchingWithOptions: 中设置观察者。在您正在观察的类型的新数据事件中,HealthKit将在实际发送相关观察者的通知之前启动您的应用程序。通过在 didFinishLaunchingWithOptions: 中设置观察者查询,您承诺在准备好观察者的同时通知您任何新数据。 https://developer.apple.com/library/ios/documentation/HealthKit/Reference/HKObserverQuery_Class/index.html#//apple_ref/doc/uid/TP40014747 - goldengil
在“终止”状态下,您能否将活动(步数、距离等)上传到服务器?我发现我的观察处理程序一直在运行,但是有时候我可以上传数据到我的服务器,有时候则不行。 - Dumber_Texan2
@Goldengil,你有一些可以分享的代码吗?目前,我所有的HealthKit API调用、Observations等都在一个名为HealthData的服务中运行。我的Observations和Observation处理程序正在工作,但我并不总是能将数据上传到我的服务器。 - Dumber_Texan2

39
这个答案有点晚了,但我希望它能帮助人们理解如何成功地使用HKObserverQuery

首先,HKObserverQuery在后台模式和完全关闭应用程序时都可以正常工作。但是,您需要首先设置一些选项才能使所有内容正常工作。

  1. 您需要在应用程序的功能中设置背景模式。请参见下面的图片:

enter image description here

  1. 然后,您需要像以下图片中那样在info.plist中添加Required Background Modes

enter image description here

  1. 您需要按照以下方式设置Background Fetch

    3.1. 从方案工具栏菜单中选择iOS模拟器或设备。

    3.2. 从同一菜单中选择编辑方案。

    3.3. 在左列中选择运行。

    3.4. 选择选项卡。

    3.5. 选择“Background Fetch”复选框,然后单击“关闭”。

enter image description here

然后,您可以使用以下代码在应用程序处于后台或关闭状态时接收通知:

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   

}

在上述方法中,HKObserver 会在用户授权 HealthKit 并激活通知后被激活。
希望这能对您有所帮助。

1
我不认为启用后台获取是获取健康存储中的更改的正确方式。获取操作会在后台启动整个应用程序,包括根视图控制器。这可能不是正确的方法,请再次确认! - Laksh Gandikota
1
我只是分享让观察者起作用的方法,至少对我来说,这只是一个答案,可能还有更好的方式,就像你所说的。我会尝试进行搜索。。。 - Victor Sigler
它对我不起作用。我已经在设备和模拟器上都尝试过了。iOS 8.4。在向 Heal 应用程序添加新数据后,通知是否应立即传递? - Kostiantyn Koval
是的,在iOS 8.4中它可以工作,但你需要省略第三步,这样它才能正常工作。如果你添加与你所设置的观察者相关的新数据到你的健康应用程序中,通知也应该会被接收到。 - Victor Sigler
当然,别担心,我会尽快将项目上传到Github,并告诉你。 - Victor Sigler
显示剩余10条评论

3
在iOS 8.1中,它确实会这样做。但是,您需要确保在应用程序委托的 application:didFinishLaunchingWithOptions:中重新创建观察者查询。由于8.0中存在错误,导致HealthKit的后台通知完全无法工作。
编辑:
在您的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];
}

2
我还没有看到一个在iOS 8.1上能够正常工作的代码示例。你能否推荐一个可以让你对后台更新做出反应的示例? - Mike Rapadas
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Cory Imdieke
这并不是问题。当您第一次打开应用程序时,所有其他需要发生的事情或需要处理的事情都会有所不同。 - drdaanger
2
你是否曾经测试过你的代码,以从watchOS2锻炼和IOS9.1中实时接收心率数据?HKObserverQuery只在启动时或iOS应用程序从后台返回前台时触发吗?这是一个BUG吗? - Ing. Ron

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接