Pourquoi HKAnchoredObjectQuery avec enableBackgroundDeliveryForType ne se triggers-t-il pas toujours lorsque l'application est en arrière-plan?

J'expérimente un peu pour me familiariser avec le HKAnchoredObjectQuery et get des résultats lorsque mon application est inactive. Je démarre l'application, passer à Apple Health, entrer un résultat de glycémie; parfois le gestionnaire de résultats est appelé immédiatement (comme en témoigne l'printing sur la console) mais d'autres fois le gestionnaire n'est pas appelé jusqu'à ce que je repasse à mon application. La même chose est vraie pour les résultats supprimés ainsi que les résultats ajoutés. Quelqu'un a des conseils?

La plupart de ce code provient d'une question de thedigitalsean adapté ici pour get des mises à jour tandis que l'application est en arrière-plan et la connection à la console. Voir: Healthkit HKAnchoredObjectQuery dans iOS 9 ne renvoie pas HKDeletedObject

class HKClient : NSObject { var isSharingEnabled: Bool = false let healthKitStore:HKHealthStore? = HKHealthStore() let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)! override init(){ super.init() } func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) { let dataTypesToRead : Set<HKObjectType> = [ glucoseType ] if(!HKHealthStore.isHealthDataAvailable()) { // let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"]) self.isSharingEnabled = false return } self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in self.isSharingEnabled = true authorizationCompleted(success: success, error: error) } } func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:uint, callback: ((source: HKClient, added: [Ssortingng]?, deleted: [Ssortingng]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!) { let queryEndDate = NSDate(timeIntervalSinceNow: NSTimeInterval(60.0 * 60.0 * 24)) let queryStartDate = NSDate.distantPast() let sampleType: HKSampleType = glucoseType as! HKSampleType let predicate: NSPredicate = HKAnchoredObjectQuery.predicateForSamplesWithStartDate(queryStartDate, endDate: queryEndDate, options: HKQueryOptions.None) var hkAnchor: HKQueryAnchor if(anchor != nil){ hkAnchor = anchor! } else { hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor)) } let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = { (query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in var added = [Ssortingng]() var deleted = [Ssortingng]() if (addedObjects?.count > 0){ for obj in addedObjects! { let quant = obj as? HKQuantitySample if(quant?.UUID.UUIDSsortingng != nil){ let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromSsortingng: "mg/dL")))! ) let msg : Ssortingng = (quant?.UUID.UUIDSsortingng)! + " " + Ssortingng(val) added.append(msg) } } } if (deletedObjects?.count > 0){ for del in deletedObjects! { let value : Ssortingng = del.UUID.UUIDSsortingng deleted.append(value) } } if(callback != nil){ callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError) } } // remove predicate to see deleted objects let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults) // added - query should be always running anchoredQuery.updateHandler = onAnchorQueryResults // added - allow query to pickup updates when app is in backgroun healthKitStore?.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate) { (success, error) in if (!success) {print("enable background error")} } healthKitStore?.executeQuery(anchoredQuery) } let AnchorKey = "HKClientAnchorKey" func getAnchor() -> HKQueryAnchor? { let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey) if(encoded == nil){ return nil } let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor return anchor } func saveAnchor(anchor : HKQueryAnchor) { let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor) NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey) NSUserDefaults.standardUserDefaults().synchronize() } } class ViewController: UIViewController { let debugLabel = UILabel(frame: CGRect(x: 10,y: 20,width: 350,height: 600)) override func viewDidLoad() { super.viewDidLoad() self.view = UIView(); self.view.backgroundColor = UIColor.whiteColor() debugLabel.textAlignment = NSTextAlignment.Center debugLabel.textColor = UIColor.blackColor() debugLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping debugLabel.numberOfLines = 0 self.view.addSubview(debugLabel) let hk = HKClient() hk.requestGlucosePermissions(){ (success, error) -> Void in if(success){ let anchor = hk.getAnchor() hk.getGlucoseSinceAnchor(anchor, maxResults: 0) { (source, added, deleted, newAnchor, error) -> Void in var msg : Ssortingng = Ssortingng() if(deleted?.count > 0){ msg += "Deleted: \n" + (deleted?[0])! for s in deleted!{ msg += s + "\n" } } if (added?.count > 0) { msg += "Added: " for s in added!{ msg += s + "\n" } } if(error != nil) { msg = "Error = " + (error?.description)! } if(msg.isEmpty) { msg = "No changes" } debugPrint(msg) if(newAnchor != nil && newAnchor != anchor){ hk.saveAnchor(newAnchor!) } dispatch_async(dispatch_get_main_queue(), { () -> Void in self.debugLabel.text = msg }) } } } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } 

J'ai également ajouté print () aux différents changements d'état de l'application. Un exemple du journal de la console (exécuté sur l'appareil iPhone 6s de XCode) montre que le gestionnaire est parfois appelé après mon input en arrière-plan, mais avant de réintégrer le premier plan et d'autres fois seulement après avoir réembedded le premier plan.

 app did become active "No changes" app will resign active app did enter background app will enter foreground "Added: E0340084-6D9A-41E4-A9E4-F5780CD2EADA 99.0\n" app did become active app will resign active app did enter background "Added: CEBFB656-0652-4109-B994-92FAA45E6E55 98.0\n" app will enter foreground "Added: E2FA000A-D6D5-45FE-9015-9A3B9EB1672C 97.0\n" app did become active app will resign active app did enter background "Deleted: \nD3124A07-23A7-4571-93AB-5201F73A4111D3124A07-23A7-4571-93AB-5201F73A4111\n92244E18-941E-4514-853F-D890F4551D76\n" app will enter foreground app did become active app will resign active app did enter background app will enter foreground "Added: 083A9DE4-5EF6-4992-AB82-7CDDD1354C82 96.0\n" app did become active app will resign active app did enter background app will enter foreground "Added: C7608F9E-BDCD-4CBC-8F32-94DF81306875 95.0\n" app did become active app will resign active app did enter background "Deleted: \n15D5DC92-B365-4BB1-A40C-B870A48A70A415D5DC92-B365-4BB1-A40C-B870A48A70A4\n" "Deleted: \n17FB2A43-0828-4830-A229-7D7DDC6112DB17FB2A43-0828-4830-A229-7D7DDC6112DB\n" "Deleted: \nCEBFB656-0652-4109-B994-92FAA45E6E55CEBFB656-0652-4109-B994-92FAA45E6E55\n" app will enter foreground "Deleted: \nE0340084-6D9A-41E4-A9E4-F5780CD2EADAE0340084-6D9A-41E4-A9E4-F5780CD2EADA\n" app did become active 

Je suggère d'utiliser un HKObserverQuery et de le configurer avec soin.

Il existe un algorithm qui surveille comment et quand vous appelez le gestionnaire "completion" de HKObserverQuery lorsque vous avez activé la diffusion en arrière-plan. Les détails de ceci sont vagues malheureusement. Quelqu'un sur les forums Apple Dev l'a appelé la règle des "3 frappes" mais Apple n'a publié aucun document que je puisse find sur son comportement.

https://forums.developer.apple.com/thread/13077

Une chose que j'ai remarquée est que, si votre application répond à une livraison en arrière-plan avec un HKObserverQuery, en créant un HKAnchoredObjectQuery, et en définissant le UpdateHandler dans cette HKAnchoredObjectQuery, ce UpdateHandler causera souvent plusieurs tirs du callback. Je soupçonnais que peut-être parce que ces callbacks supplémentaires sont exécutés APRÈS avoir déjà dit à Apple que vous avez terminé votre travail en réponse à la livraison de fond, vous appelez le gestionnaire de complétion plusieurs fois et peut-être ils vous appellent less souvent pour un mauvais comportement.

J'ai eu le plus de succès en obtenant des callbacks consistants en faisant ce qui suit:

  1. Utiliser un ObserverQuery et s'assurer que l'appel du gestionnaire "completion" est appelé une fois et à la fin de votre travail.
  2. Ne pas définir un gestionnaire de mise à jour dans mon HKAnchoredObjectQuery en cours d'exécution en arrière-plan (aide à atteindre 1).
  3. Mettre l'accent sur la création de mes gestionnaires de requêtes, AppDelegate et ViewController est aussi rapide que possible. J'ai remarqué que lorsque j'ai ramené tous mes callbacks à une seule instruction d'printing, les callbacks de HealthKit sont arrivés immédiatement et de manière plus cohérente. Cela dit, Apple accorde une attention particulière au time d'exécution. Donc, essayez de déclarer les choses de façon statique lorsque c'est possible et concentrez-vous sur la vitesse.

Je suis depuis passé à mon projet d'origine qui utilise Xamarin.iOS, pas rapide, donc je n'ai pas suivi le code que j'ai posté à l'origine. Mais voici une version mise à jour (et non testée) de ce code qui devrait tenir count de ces changements (à l'exception des améliorations de vitesse):

 // // HKClient.swift // HKTest import UIKit import HealthKit class HKClient : NSObject { var isSharingEnabled: Bool = false let healthKitStore:HKHealthStore? = HKHealthStore() let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)! override init(){ super.init() } func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) { let dataTypesToRead : Set<HKObjectType> = [ glucoseType ] if(!HKHealthStore.isHealthDataAvailable()) { // let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"]) self.isSharingEnabled = false return } self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in self.isSharingEnabled = true authorizationCompleted(success: success, error: error) } } func startBackgroundGlucoseObserver( maxResultsPerQuery: Int, anchorQueryCallback: ((source: HKClient, added: [Ssortingng]?, deleted: [Ssortingng]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!)->Void { let onBackgroundStarted = {(success: Bool, nsError : NSError?)->Void in if(success){ //Background delivery was successfully created. We could use this time to create our Observer query for the system to call when changes occur. But we do it outside this block so that even when background deliveries don't work, //we will have the observer query working when are in the foreground at least. } else { debugPrint(nsError) } let obsQuery = HKObserverQuery(sampleType: self.glucoseType as! HKSampleType, predicate: nil) { query, completion, obsError in if(obsError != nil){ //Handle error debugPrint(obsError) abort() } var hkAnchor = self.getAnchor() if(hkAnchor == nil) { hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor)) } self.getGlucoseSinceAnchor(hkAnchor, maxResults: maxResultsPerQuery, callContinuosly:false, callback: { (source, added, deleted, newAnchor, error) -> Void in anchorQueryCallback(source: self, added: added, deleted: deleted, newAnchor: newAnchor, error: error) //Tell Apple we are done handling this event. This needs to be done inside this handler completion() }) } self.healthKitStore?.executeQuery(obsQuery) } healthKitStore?.enableBackgroundDeliveryForType(glucoseType, frequency: HKUpdateFrequency.Immediate, withCompletion: onBackgroundStarted ) } func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:Int, callContinuosly:Bool, callback: ((source: HKClient, added: [Ssortingng]?, deleted: [Ssortingng]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!){ let sampleType: HKSampleType = glucoseType as! HKSampleType var hkAnchor: HKQueryAnchor; if(anchor != nil){ hkAnchor = anchor! } else { hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor)) } let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = { (query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in var added = [Ssortingng]() var deleted = [Ssortingng]() if (addedObjects?.count > 0){ for obj in addedObjects! { let quant = obj as? HKQuantitySample if(quant?.UUID.UUIDSsortingng != nil){ let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromSsortingng: "mg/dL")))! ) let msg : Ssortingng = (quant?.UUID.UUIDSsortingng)! + " " + Ssortingng(val) added.append(msg) } } } if (deletedObjects?.count > 0){ for del in deletedObjects! { let value : Ssortingng = del.UUID.UUIDSsortingng deleted.append(value) } } if(callback != nil){ callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError) } } let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults) if(callContinuosly){ //The updatehandler should not be set when responding to background observerqueries since this will cause multiple callbacks anchoredQuery.updateHandler = onAnchorQueryResults } healthKitStore?.executeQuery(anchoredQuery) } let AnchorKey = "HKClientAnchorKey" func getAnchor() -> HKQueryAnchor? { let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey) if(encoded == nil){ return nil } let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor return anchor } func saveAnchor(anchor : HKQueryAnchor) { let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor) NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey) NSUserDefaults.standardUserDefaults().synchronize() } }