So, I am trying to get my app to read in HealthKit
data. I have a function that I call from the main app view controller which causes a query in another class for all the health data in that month. Theres then a few calculations before the array of data is returned from a separate function in the calculation class, to a separate function in the view controller.
The queries take around 2 seconds each due to the volume of data. I would like to be able to set them off asynchronously and when they have all returned, I can update the UI.
The problem is, I call the function for each month, which goes and starts the HKSampleQueries
, but they don't return in order, and the time that it takes for them to return varies. This means that I end up with variables being changed halfway through one set of datas calculations because the next set has just started.
I only know two ways round this:
Set a delay before calling each calculation like this:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{}
But that wastes app time
Or I could just duplicate the code several times and call different class for each month. But that seems stupid and inefficient.
So question is. How do I effectively share code that will run several times with different variables each time. Cheers
Example of function:
In View controller:
HeartRateCalculator *commonClassTwo =[[HeartRateCalculator alloc] init];
[commonClassTwo calculateData:0];
[commonClassTwo calculateData:-1];
[commonClassTwo calculateData:-2];
In HeartRateCalculator
-(void)calculateData:(NSInteger)monthsBack{
//Some other stuff
//Generate monthPeriodPredicate based on monthsBack integer
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:heartRate predicate:monthPeriodPredicate limit:200000 sortDescriptors:@[timeSortDescriptor] resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
//Finish Calculations, call other functions (ie. [self doThis];) and then return
//When calculations return, monthPeriodPredicate is always value of the last predicate to be called, not the one that the HKSampleQuery was made with.
}
[healthStoreFive executeQuery:query];
Full Code:
-(void)calculateData:(NSInteger)monthsBack withCompletionBlock:(void(^)())completionBlock {//0 Means only current month, 2 means this month and last month and month before
//for(NSInteger i=0; i>=monthsBack; i--){
//monthForCalculation = monthsBack;
NSDateComponents *components = [[NSCalendar currentCalendar] components: NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond fromDate:[NSDate date]];
NSDateComponents *adjustableComponent = [[NSDateComponents alloc] init];
[adjustableComponent setMonth:monthsBack];
[adjustableComponent setDay:-[components day]+1];
[adjustableComponent setHour:-[components hour]];
[adjustableComponent setMinute:-[components minute]];
[adjustableComponent setSecond:-[components second]];
startOfMonth = [[NSCalendar currentCalendar] dateByAddingComponents:adjustableComponent toDate:[NSDate date] options:0];
adjustableComponent = [[NSDateComponents alloc] init];
[adjustableComponent setMonth:1];
NSDate *endOfMonth = [[NSCalendar currentCalendar] dateByAddingComponents:adjustableComponent toDate:startOfMonth options:0];
NSDate *secondEarlier = [endOfMonth dateByAddingTimeInterval:-1];
components = [[NSCalendar currentCalendar] components: NSCalendarUnitDay fromDate:secondEarlier];
daysInMonth = [components day];
NSPredicate *monthPeriodPredicate = [HKQuery predicateForSamplesWithStartDate:startOfMonth endDate:endOfMonth options:HKQueryOptionStrictStartDate];
healthStoreFive = [[HKHealthStore alloc] init];
HKQuantityType *heartRate = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeartRate];
NSSortDescriptor *timeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:HKSampleSortIdentifierEndDate ascending:NO];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:heartRate predicate:monthPeriodPredicate limit:200000 sortDescriptors:@[timeSortDescriptor] resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
NSMutableArray *dataValues = [[NSMutableArray alloc] init];
NSMutableArray *dataDates = [[NSMutableArray alloc] init];
for (HKQuantitySample *sample in results) {
[dataValues addObject:[NSNumber numberWithFloat:[sample.quantity doubleValueForUnit:[[HKUnit countUnit] unitDividedByUnit:[HKUnit minuteUnit]]]]];
[dataDates addObject:sample.startDate];
}
monthForCalculation = monthsBack;
chronologicalDataValues = [[NSMutableArray alloc] init];
chronologicalDataDates = [[NSMutableArray alloc] init];
chronologicalDataValues = [[[dataValues reverseObjectEnumerator] allObjects] mutableCopy];
chronologicalDataDates = [[[dataDates reverseObjectEnumerator] allObjects] mutableCopy];
//dispatch_async(dispatch_get_main_queue(), ^{
if(dataDates.count == 0){
ViewController *commonClass =[[ViewController alloc] init];
[commonClass receiveCalculationData:[[NSMutableArray alloc] init] array:[[NSMutableArray alloc] init] daysToDisplay:[[NSMutableArray alloc] init] chosenMonth:monthForCalculation];
}
else{
NSLog(@"%@", [dataDates objectAtIndex:dataDates.count-1]);
NSLog(@"%@", [dataDates objectAtIndex:0]);
[self calculateDayStringsFromData];
}
completionBlock();
//});
}];
NSLog(@"HKSampleQuery Requested For Heart Rate Data");
[healthStoreFive executeQuery:query];
// } }
You can use a
dispatch_group
in order to schedule a block to fire after all of your tasks are completed.You just have to modify your
calculateData:
method to accept adispatch_group_t
argument (you can always add a completion block as well if needed):Then you can just call it like so:
It's the same principle that Warif was going for, but
dispatch_groups
are much more elegant than using your own variable to track the number of tasks executing.Although I'm not sure what you mean when you say that you want the tasks to be asynchronously executed. From the Apple docs on
HKSampleQuery
:Therefore your tasks are already asynchronous.