How to get cycling duration from HealthKit for ios app using Swift - ios

I am creating an app where I am reading data from health-Kit. I am able to read number of steps, Running + Walking etc. now i am trying to read the date and duration of cycling. This is what I was using for Running + Walking
func readDistanceWalkingRunning(completion: (([AnyObject]!, NSError!) -> Void)!) {
let runningWalking = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
let predicate = HKQuery.predicateForSamplesWithStartDate(NSDate().dateByAddingTimeInterval(-86400.0), endDate: NSDate(), options: HKQueryOptions.None)
let stepsSampleQuery = HKSampleQuery(sampleType: runningWalking!,
predicate: predicate,
limit: 100,
sortDescriptors: nil)
{ [weak self] (query, results, error) in
if let results = results as? [HKQuantitySample] {
for result in results {
print(" Distance was " + " \(result.quantity.doubleValueForUnit(HKUnit.mileUnit())) ")
print("Date was " + "\(result.startDate)")
}
}
}
// Don't forget to execute the Query!
executeQuery(stepsSampleQuery)
}
And everything is fine, But when I try to read distance for cycling using the code below, I am getting nil result. Cycling data is showing in appleHealth app but Why am I getting result as nil ? Please help
func readDistanceCycling(completion: (([AnyObject]!, NSError!) -> Void)!) {
let distanceCycling = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceCycling)
let predicate = HKQuery.predicateForSamplesWithStartDate(NSDate(), endDate: NSDate(), options: HKQueryOptions.None)
let query = HKSampleQuery(sampleType: distanceCycling!, predicate: predicate, limit: 100, sortDescriptors: nil, resultsHandler: { query, result, error in
if result != nil
{
print("We have some Data")
}
else
{
print("Result is nil")
}
if let results = result as? [HKQuantitySample] {
for result in results {
print(" Quantity type " + " \(result.quantityType) ")
print("Date was " + "\(result.startDate)")
}
}
})
executeQuery(query)
}
}

Use given below function. It give you leatest 7 record. According to your requirement you can also change it.
func getHealthDataValue ( HealthQuantityType : HKQuantityType , strUnitType : String , GetBackFinalhealthData: ((( healthValues : [AnyObject] ) -> Void)!) )
{
if let heartRateType = HKQuantityType.quantityTypeForIdentifier(HealthQuantityType.identifier)
{
if (HKHealthStore.isHealthDataAvailable() ){
let sortByTime = NSSortDescriptor(key:HKSampleSortIdentifierEndDate, ascending:false)
// let timeFormatter = NSDateFormatter()
// timeFormatter.dateFormat = "hh:mm:ss"
//yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
let query = HKSampleQuery(sampleType:heartRateType, predicate:nil, limit:7, sortDescriptors:[sortByTime], resultsHandler:{(query, results, error) in
guard let results = results else {
//include the healthkit error in log
if let errorDescription = error!.description as String?
{
GetBackFinalhealthData (healthValues: ["nodata"])
}
return
}
var arrHealthValues = [AnyObject]()
for quantitySample in results {
let quantity = (quantitySample as! HKQuantitySample).quantity
let healthDataUnit : HKUnit
if (strUnitType.length > 0 ){
healthDataUnit = HKUnit(fromString: strUnitType)
}else{
healthDataUnit = HKUnit.countUnit()
}
let tempActualhealthData = "\(quantity.doubleValueForUnit(healthDataUnit))"
let tempActualRecordedDate = "\(dateFormatter.stringFromDate(quantitySample.startDate))"
if (tempActualhealthData.length > 0){
let dicHealth : [String:AnyObject] = [HealthValue.kIdentifierValue :tempActualhealthData , HealthValue.kRecordDate :tempActualRecordedDate , HealthValue.kIdentifierDisplayUnit : strUnitType ]
arrHealthValues.append(dicHealth)
}
}
if (arrHealthValues.count > 0)
{
GetBackFinalhealthData (healthValues: arrHealthValues)
}
else
{
GetBackFinalhealthData (healthValues: [HealthValue.kNoData])
}
})
(self.HealthStore as! HKHealthStore).executeQuery(query)
}
}
}
Use above function as follow.
Here you need to pass a type and unit.
self.getHealthDataValue(HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodPressureDiastolic), strUnitType: "mmHg"
) { (arrHealth) -> Void in
}

Related

How to get HealthKit total steps for previous dates

I am trying to get steps from Health Kit, its working fine for mobile but when i connect Apple Watch my app get more steps then Health kit. i trace it and find that it collect detail record of steps but total steps are less then detail in Health kit.
My App getting the sum of these steps:
But I want To Get these:
Here is My Code:
func MultipleDaysStepsAndActivitiesTest(_ startDate:Date, completion: #escaping (NSDictionary, [HealthKitManuallActivity], NSError?) -> () ) {
let type = HKSampleType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount) // The type of data we are requesting
let now = Date()
let newDate = startDate
let predicate = HKQuery.predicateForSamples(withStart: newDate, end: now, options: HKQueryOptions())
var dates = now.datesBetweenGivenDates(startDate,endDate:now)
dates = dates.reversed()
let query = HKSampleQuery(sampleType: type!, predicate: predicate, limit: 0, sortDescriptors: nil) { query, results, error in
var dict:[String:Double] = [:]
if results?.count > 0 {
for result in results as! [HKQuantitySample] {
print(result)
if result.sourceRevision.source.name != kHealthKitSource {
if dict[self.fmt.string(from: result.startDate)] != nil {
dict[self.fmt.string(from: result.startDate)] = dict[self.fmt.string(from: result.startDate)]! + result.quantity.doubleValue(for: HKUnit.count())
} else {
dict[self.fmt.string(from: result.startDate)] = result.quantity.doubleValue(for: HKUnit.count())
}
}
}
}
var sDate = startDate // first date
let cal = Calendar.current
print(dict)
if dict.isEmpty {
while sDate <= Date() {
dict[self.fmt.string(from: sDate)] = 0
sDate = cal.date(byAdding: .day, value: 1, to: sDate)!
}
} else {
while sDate <= Date() {
if dict[self.fmt.string(from: sDate)] == nil {
dict[self.fmt.string(from: sDate)] = 0
}
sDate = cal.date(byAdding: .day, value: 1, to: sDate)!
}
}
// reading activities
self.MultipleDaysWorkouts(startDate, endDate: now, completion: { (activities, error) in
if results?.count == 0 {
for activity in activities {
dict[activity.startDate] = 0.0
}
}
// reading mindfulness activities
self.MultipleDayMindFullnessActivity(startDate, completion: { (mindfulnessActivities, mindError) in
if mindError == nil {
let allActivities = mindfulnessActivities + activities
completion(dict as NSDictionary, allActivities, mindError as NSError?)
}
})
})
}
execute(query)
}
You should use HKStatisticsQuery or HKStatisticsCollectionQuery instead of HKSampleQuery. The statistics queries will de-deuplicate overlapping step samples from different sources to ensure that you do not double-count them. You can find documentation for them here and here.

Getting yesterdays steps from HealthKit

I'm building an app for personal use, and I am currently stuck on how to accurately get yesterdays steps from the healthkit. And then from there, placing it into a variable (should be easy, I know).
I have a HealthKitManager class that calls the function from inside a view, and then appends that to a variable from that same view.
I have scoured most of the healthKit questions, and I get back data, but I don't think it is accurate data. My phone data from yesterday is 1442 steps, but it's returning 2665 steps. On top of that, when I try to put the data is a variable it prints out as 0.
HealthKitManagerClass
import Foundation
import HealthKit
class HealthKitManager {
let storage = HKHealthStore()
init()
{
checkAuthorization()
}
func checkAuthorization() -> Bool
{
// Default to assuming that we're authorized
var isEnabled = true
// Do we have access to HealthKit on this device?
if HKHealthStore.isHealthDataAvailable()
{
// We have to request each data type explicitly
let steps = NSSet(object: HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!)
// Now we can request authorization for step count data
storage.requestAuthorizationToShareTypes(nil, readTypes: steps as? Set<HKObjectType>) { (success, error) -> Void in
isEnabled = success
}
}
else
{
isEnabled = false
}
return isEnabled
}
func yesterdaySteps(completion: (Double, NSError?) -> ())
{
// The type of data we are requesting (this is redundant and could probably be an enumeration
let type = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
// Our search predicate which will fetch data from now until a day ago
// (Note, 1.day comes from an extension
// You'll want to change that to your own NSDate
let calendar = NSCalendar.currentCalendar()
let yesterday = calendar.dateByAddingUnit(.Day, value: -1, toDate: NSDate(), options: [])
//this is probably why my data is wrong
let predicate = HKQuery.predicateForSamplesWithStartDate(yesterday, endDate: NSDate(), options: .None)
// The actual HealthKit Query which will fetch all of the steps and sub them up for us.
let query = HKSampleQuery(sampleType: type!, predicate: predicate, limit: 0, sortDescriptors: nil) { query, results, error in
var steps: Double = 0
if results?.count > 0
{
for result in results as! [HKQuantitySample]
{
steps += result.quantity.doubleValueForUnit(HKUnit.countUnit())
}
}
//I'm unsure if this is correct as well
completion(steps, error)
print("\(steps) STEPS FROM HEALTH KIT")
//this adds the steps to my character (is this in the right place)
Player.User.Gold.addSteps(Int(steps))
}
//not 100% on what this does, but I know it is necessary
storage.executeQuery(query)
}}
ViewControllerClass
import UIKit
import Foundation
class UpdateViewController: UIViewController {
#IBOutlet var back: UIButton!
let HKM = HealthKitManager()
var stepsFromPhone = Double()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
back.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
HKM.yesterdaySteps(){ steps, error in
self.stepsFromPhone = steps
}
Player.User.Gold.addSteps(Int(stepsFromPhone))
print(Player.User.Gold.getSteps(), "IN PLAYER")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The output from
print(Player.User.Gold.getSteps(), "IN PLAYER")
is
0 IN PLAYER
The output from
print("\(steps) STEPS FROM HEALTH KIT")
is
2665.0 STEPS FROM HEALTH KIT
so, basically my questions are:
what NSDate() do I need for the whole of yesterday?
how do I take the steps from the yesterdaySteps() and correctly place them into a variable in the UpdateViewController?
Thank you for any help!
This is the method I am using in my healthStore class
func TodayTotalSteps(completion: (stepRetrieved: Double) -> Void) {
let type = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount) // The type of data we are requesting
let calendar = NSCalendar.currentCalendar()
let interval = NSDateComponents()
interval.day = 1
let anchorComponents = calendar.components([.Day , .Month , .Year], fromDate: NSDate())
anchorComponents.hour = 0
let anchorDate = calendar.dateFromComponents(anchorComponents)
let stepsQuery = HKStatisticsCollectionQuery(quantityType: type!, quantitySamplePredicate: nil, options: .CumulativeSum, anchorDate: anchorDate!, intervalComponents: interval)
stepsQuery.initialResultsHandler = {query, results, error in
let endDate = NSDate()
var steps = 0.0
let startDate = calendar.dateByAddingUnit(.Day, value: 0, toDate: endDate, options: [])
if let myResults = results{ myResults.enumerateStatisticsFromDate(startDate!, toDate: endDate) { statistics, stop in
if let quantity = statistics.sumQuantity(){
let date = statistics.startDate
steps = quantity.doubleValueForUnit(HKUnit.countUnit())
// print("\(date): steps = \(steps)")
}
completion(stepRetrieved: steps)
}
} else {
completion(stepRetrieved: steps)
}
}
executeQuery(stepsQuery)
}
and here is How I am using it
func getStepsData() {
// I am sendng steps to my server thats why using this variable
var stepsToSend = 0
MyHealthStore.sharedHealthStore.todayManuallyAddedSteps({ (steps , error) in
if error != nil{
// handle error
}
else{
// truncating manuall steps
MyHealthStore.sharedHealthStore.TodayTotalSteps({ (stepRetrieved) in
stepsToSend = Int(stepRetrieved - steps)
})
}
})
}
and here is the function used above for manually added steps which we are truncating in order to get exact steps
func todayManuallyAddedSteps(completion: (Double, NSError?) -> () )
{
let type = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount) // The type of data we are requesting
let date = NSDate()
let cal = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let newDate = cal.startOfDayForDate(date)
let predicate = HKQuery.predicateForSamplesWithStartDate(newDate, endDate: NSDate(), options: .None) // Our search predicate which will fetch all steps taken today
// The actual HealthKit Query which will fetch all of the steps and add them up for us.
let query = HKSampleQuery(sampleType: type!, predicate: predicate, limit: 0, sortDescriptors: nil) { query, results, error in
var steps: Double = 0
if results?.count > 0
{
for result in results as! [HKQuantitySample]
{
// checking and adding manually added steps
if result.sourceRevision.source.name == "Health" {
// these are manually added steps
steps += result.quantity.doubleValueForUnit(HKUnit.countUnit())
}
else{
// these are auto detected steps which we do not want from using HKSampleQuery
}
}
completion(steps, error)
} else {
completion(steps, error)
}
}
executeQuery(query)
}
I hope it helps. Let me know if you face any issue.
You can use HKStatisticsQuery
let quantityType = HKSampleType.quantityType(forIdentifier: .stepCount)!
let predicate = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate,
options: [.strictStartDate, .strictEndDate]
)
let query = HKStatisticsQuery(
quantityType: quantityType,
quantitySamplePredicate: predicate,
options: .cumulativeSum) { (query, result, error) in
guard let result = result, error == nil else {
print("HeathService error \(String(describing: error))")
return
}
callback(result)
}

Query HealthKit for HKCategoryTypeIdentifierSleepAnalysis

I have built a method that imports a sleep sample but I can't get it to return the proper value for hours asleep.
The method to query for sleep data looks like this:
func updateHealthCategories() {
let categoryType = HKObjectType.categoryTypeForIdentifier(HKCategoryTypeIdentifierSleepAnalysis)
let start = NSDate(dateString:"2015-11-04")
let end = NSDate(dateString:"2015-11-05")
let categorySample = HKCategorySample(type: categoryType!,
value: HKCategoryValueSleepAnalysis.Asleep.rawValue,
startDate: start,
endDate: end)
self.hoursSleep = Double(categorySample.value)
print(categorySample.value)
}
The date is formatted like this:
extension NSDate
{
convenience
init(dateString:String) {
let dateStringFormatter = NSDateFormatter()
dateStringFormatter.dateFormat = "yyyy-MM-dd"
dateStringFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
let d = dateStringFormatter.dateFromString(dateString)!
self.init(timeInterval:0, sinceDate:d)
}
}
I'm calling data from November 4-5, which contains this data:
However, the categorySample.value returns 1 instead of 3.
The value you are accessing is the category sample value, an HKCategoryType, and not the number of hours of sleep.
The definition for HKCategoryTypeIdentifierSleepAnalysis
typedef enum : NSInteger {
HKCategoryValueSleepAnalysisInBed,
HKCategoryValueSleepAnalysisAsleep,
} HKCategoryValueSleepAnalysis;
defines two possible values, 0 or 1 where the value of 1 matches HKCategoryValueSleepAnalysisAsleep.
Getting the hours asleep requires setting up a HKSampleQuery.
The code looks something like this:
if let sleepType = HKObjectType.categoryTypeForIdentifier(HKCategoryTypeIdentifierSleepAnalysis) {
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: .None)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: 30, sortDescriptors: [sortDescriptor]) { (query, tmpResult, error) -> Void in
if let result = tmpResult {
for item in result {
if let sample = item as? HKCategorySample {
let value = (sample.value == HKCategoryValueSleepAnalysis.InBed.rawValue) ? "InBed" : "Asleep"
print("sleep: \(sample.startDate) \(sample.endDate) - source: \(sample.source.name) - value: \(value)")
let seconds = sample.endDate.timeIntervalSinceDate(sample.startDate)
let minutes = seconds/60
let hours = minutes/60
}
}
}
}
healthStore.executeQuery(query)
}
I summarized this from http://benoitpasquier.fr/sleep-healthkit/.
Here is Swift 5, iOS 16 compatible answer if someone is still looking. You can parse/operate data as per your needs.
func getSleepAnalysis() {
let healthStore = HKHealthStore()
let endDate = Date()
guard let startDate = Calendar.current.date(byAdding: .day, value: -7, to: endDate) else {
fatalError("*** Unable to create the start date ***")
}
// first, we define the object type we want
guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
return
}
// we create a predicate to filter our data
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
// I had a sortDescriptor to get the recent data first
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
// we create our query with a block completion to execute
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: Int(HKObjectQueryNoLimit), sortDescriptors: [sortDescriptor]) { (query, result, error) in
if error != nil {
// handle error
return
}
if let result = result {
// do something with those data
result
.compactMap({ $0 as? HKCategorySample })
.forEach({ sample in
guard let sleepValue = HKCategoryValueSleepAnalysis(rawValue: sample.value) else {
return
}
let isAsleep = sleepValue == .asleep
print("HealthKit sleep \(sample.startDate) \(sample.endDate) - source \(sample.sourceRevision.source.name) - isAsleep \(isAsleep)")
})
}
}
// finally, we execute our query
healthStore.execute(query)
}
I hope you'll get the authorization for SleepAnalysis before this so data is retrived.

Healthkit HKAnchoredObjectQuery in iOS 9 not returning HKDeletedObject

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

HKStatisticsCollectionQuery resultsHandler and NSOperation

In my project I am creating HKStatisticsCollectionQueries for a series of HKQuantityTypes. The resultsHandler then adds this data to an date-ordered array of objects. I want to do another operation only when the entire series of HKStatisticsCollectionQueries have been processed and the results appended to my array.
I have tried to do this by putting the task inside of a subclass of NSOperation, but the dependent block is fired before any of the samples are added to the array. According to the HKStatisticsCollectionQuery documentation "This method runs the query on an anonymous background queue. When the query is complete, it executes the results handler on the same background queue"
Is there a way to use HKStatisticsCollectionQuery's initialResultsHandler and statisticsUpdateHandler with NSOperation?
when I run this I get the following output:
cycleOperation start
cycleOperation CompletionBlock
dependentOperation start
dependentOperation CompletionBlock
SumStatistics addSamplesToArray: cycle: 96 samples added
SumStatistics main complete: 96 samples added
func getCycleKm(){
let sampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceCycling)
let hkUnit = HKUnit.meterUnitWithMetricPrefix(.Kilo)
println("cycleOperation start")
let cycleOperation = SumStatistics(quantityType: sampleType, startDate: startingDate, heathDataArray: self.healthDataArray)
cycleOperation.completionBlock = {println("cycleOperation CompletionBlock ")}
let dependentOperation = NSBlockOperation{()->Void in println("dependentOperation start")}
dependentOperation.completionBlock = {println("dependentOperation CompletionBlock")}
dependentOperation.addDependency(cycleOperation)
self.operationQueue.addOperation(cycleOperation)
self.operationQueue.addOperation(dependentOperation)
}
class SumStatistics:NSOperation{
let healthKitStore:HKHealthStore = HKHealthStore()
private let quantityType:HKQuantityType
private let startDate:NSDate
private let endDate: NSDate
private let statsOption: HKStatisticsOptions
var healthDataArray:[HealthData]
required init(quantityType:HKQuantityType, startDate:NSDate, heathDataArray:[HealthData]){
self.quantityType = quantityType
self.startDate = startDate
let startOfToday = NSDate().getStartOfDate()
self.endDate = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startOfToday, options: nil)!
self.statsOption = HKStatisticsOptions.CumulativeSum
self.healthDataArray = heathDataArray
super.init()
}
override func main() {
getSumStatistics { (hkSamples, statsError) -> Void in
self.addSamplesToArray(hkSamples)
println("SumStatistics main complete: \(hkSamples.count) samples added")
}
}
func addSamplesToArray(newSamples:[HKQuantitySample]){
var samples = newSamples
samples.sort({$0.startDate.timeIntervalSinceNow > $1.startDate.timeIntervalSinceNow})
if samples.count == 0{
println("SumStatistics addSamplesToArray: no samples!")
return
}
var ctr = 0
var typeString = ""
for healthDataDate in self.healthDataArray{
while healthDataDate.date.isSameDate(samples[ctr].startDate) && ctr < samples.count - 1{
switch samples[ctr].quantityType.identifier {
case HKQuantityTypeIdentifierBodyMass:
healthDataDate.weight = samples[ctr].quantity
typeString = "weight"
case HKQuantityTypeIdentifierDietaryEnergyConsumed:
healthDataDate.dietCalories = samples[ctr].quantity
typeString = "diet"
case HKQuantityTypeIdentifierActiveEnergyBurned:
healthDataDate.activeCalories = samples[ctr].quantity
typeString = "active"
case HKQuantityTypeIdentifierBasalEnergyBurned:
healthDataDate.basalCalories = samples[ctr].quantity
typeString = "basal"
case HKQuantityTypeIdentifierStepCount:
healthDataDate.steps = samples[ctr].quantity
typeString = "steps"
case HKQuantityTypeIdentifierDistanceCycling:
healthDataDate.cycleKM = samples[ctr].quantity
typeString = "cycle"
case HKQuantityTypeIdentifierDistanceWalkingRunning:
healthDataDate.runWalkKM = samples[ctr].quantity
typeString = "runWalk"
default:
println("SumStatistics addSamplesToArray type not found -> \(samples[ctr].quantityType)")
}
if ctr < samples.count - 1{
ctr += 1
}else{
break
}
}
}
println("SumStatistics addSamplesToArray: \(typeString): \(newSamples.count) samples added")
}
func getSumStatistics(completionHandler:([HKQuantitySample], NSError!)->Void){
let dayStart = NSCalendar.currentCalendar().startOfDayForDate(startDate)
let addDay = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: endDate, options:nil)
let dayEnd = NSCalendar.currentCalendar().startOfDayForDate(addDay!) //add one day
let interval = NSDateComponents()
interval.day = 1
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.None)
let newQuery = HKStatisticsCollectionQuery(quantityType: quantityType,
quantitySamplePredicate: predicate,
options: statsOption,
anchorDate: dayStart,
intervalComponents: interval)
newQuery.initialResultsHandler = {
query, statisticsCollection, error in
var resultsArray = [HKQuantitySample]()
if error != nil {
println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
}else{
statisticsCollection.enumerateStatisticsFromDate(self.startDate, toDate: self.endDate, withBlock: { (statistics, stop) -> Void in
if let statisticsQuantity = statistics.sumQuantity() {
let startD = NSCalendar.currentCalendar().startOfDayForDate(statistics.startDate)
let endD = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startD, options: nil)
let qSample = HKQuantitySample(type: self.quantityType, quantity: statisticsQuantity, startDate: startD, endDate: endD)
resultsArray.append(qSample)
}
})
}
completionHandler(resultsArray,error)
}
newQuery.statisticsUpdateHandler = {
query, statistics, statisticsCollection, error in
println("*** updateHandler fired")
var resultsArray = [HKQuantitySample]()
if error != nil {
println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
}else{
statisticsCollection.enumerateStatisticsFromDate(self.startDate, toDate: self.endDate, withBlock: { (statistics, stop) -> Void in
if let statisticsQuantity = statistics.sumQuantity() {
let startD = NSCalendar.currentCalendar().startOfDayForDate(statistics.startDate)
let endD = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startD, options: nil)
let qSample = HKQuantitySample(type: self.quantityType, quantity: statisticsQuantity, startDate: startD, endDate: endD)
resultsArray.append(qSample)
}
})
}
completionHandler(resultsArray,error)
}
self.healthKitStore.executeQuery(newQuery)
}
}
The short answer was RTFM! (more carefully). I am leaving my original question and code as is, and adding the solution here.
Thanks to this post for helping me figure this out: http://szulctomasz.com/ios-second-try-to-nsoperation-and-long-running-tasks/ And of course, as I found, there is no substitute for a careful reading of the Reference: https://developer.apple.com/library/ios/documentation/Cocoa/Reference/NSOperation_class/
The problem was that I was not subclassing NSOperation properly for running concurrent operations. One must add the concurrent operations to start() rather than main() and then use KVO to update the finished and executing properties, which are what signals that an operation is complete.
I needed to modify the class above to include:
private var _executing = false
private var _finished = false
override var executing:Bool{return _executing}
override var finished:Bool{return _finished}
override func cancel() {
super.cancel()
finish()
}
override func start() {
if cancelled{
finish()
return
}
willChangeValueForKey("isExecuting")
_executing = true
didChangeValueForKey("isExecuting")
getSumStatistics { (hkSamples, statsError) -> Void in
println("LoadStatistics getStatistics completion: \(hkSamples.count) samples")
self.addSamplesToArray(hkSamples, completionHandler: { (success) -> Void in
println("LoadStatistics addSamplesToArray completion")
self.finish()
})
self.completion(true)
}
main()
}
func finish(){
willChangeValueForKey("isExecuting")
willChangeValueForKey("isFinished")
_executing = false
_finished = true
didChangeValueForKey("isExecuting")
didChangeValueForKey("isFinished")
}
override func main() {
if cancelled == true && _finished != false{
finish()
return
}
}
Now I get!
cycleOperation start
LoadStatistics getStatistics completion: 97 samples
LoadStatistics addSamplesToArray completion
cycleOperation CompletionBlock
dependentOperation start
dependentOperation CompletionBlock

Resources