Healthkit HKAnchoredObjectQuery in iOS 9 not retur

2019-06-08 21:55发布

问题:

I was excited to learn that Apple added tracking of deletes in HealthKit in iOS 9. So I set up a test project to try it out. Unfortunately, while I can get new data just fine, I am not getting any deleted objects in my callbacks.

I have a functioning HKAnchoredObjectQuery that tracks HKQueryAnchor and gives me new HKSamples whenever I add a BloodGlucose quantity into HealthKit via the Health app. However when I delete that same quantity and re-run the app the HKDeletedObject is always empty. Even I do an add and a delete at the same time. It seems no matter what I do, the HKDeletedObject array is always empty. But additions work fine (only getting the added samples since the last anchor).

Here is my code. It is just 2 files. To recreate the project just make a new swift project, give yourself the HealthKit Entitlement, and copy these in. (Note: When you run it, you only get one update for each run so if you make changes in HealthKit you have to stop and restart the app to test the callbacks.)

This is my HealthKit client:

//
//  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 getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:uint, callback: ((source: HKClient, added: [String]?, deleted: [String]?, 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 = [String]()
            var deleted = [String]()

            if (addedObjects?.count > 0){
                for obj in addedObjects! {
                    let quant = obj as? HKQuantitySample
                    if(quant?.UUID.UUIDString != nil){
                        let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromString: "mg/dL")))! )
                        let msg : String = (quant?.UUID.UUIDString)! + " " + String(val)
                        added.append(msg)
                    }
                }
            }

            if (deletedObjects?.count > 0){
                for del in deletedObjects! {
                    let value : String = del.UUID.UUIDString
                    deleted.append(value)
                }
            }

            if(callback != nil){
                callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError)
            }
        }

        let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: predicate, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: 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()
    }
}

This is my View:

//
//  ViewController.swift
//  HKTest

import UIKit
import HealthKit

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 : String = String()

                        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.
    }
}

Note: I know Apple recommends to set this up using an HKObserverQuery. I originally did it that way in a Xamarin project and the behavior was the same (no HKDeletedObjects were getting sent). So when trying it out with swift I left out the HKObserverQuery for simplicity.

回答1:

Remove the predicate (predicate: nil) when you instantiate the query and you'll see more results, including deleted results. The first time you run this (before the HKQueryAnchor has been saved) you'll get all the results so you may want to do something to filter those; but subsequent execution of the query will use your saved anchor so you'll only see changes since that saved anchor.

You probably also want to set the updateHandler property on your query before executing. This will set the query up to run continuously in the background calling the update handler whenever there are changes (Adds or Deletes). The code near the end of getGlucoseSinceAnchor(...) looks like

...
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults)
anchoredQuery.updateHandler = onAnchorQueryResults
healthKitStore?.executeQuery(anchoredQuery)
...