Search code examples
iosobjective-ciphonehealthkithksamplequery

Running Some Code Multiple Times Asynchronously With Different Variables


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];

// } }


Solution

  • 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 a dispatch_group_t argument (you can always add a completion block as well if needed):

    - (void)calculateData:(NSInteger)monthsBack group:(dispatch_group_t)group {
    
        dispatch_group_enter(group); // increment group task count
    
        //Some other stuff
    
        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
    
            dispatch_group_leave(group); // decrement task count
        }];
    
        [healthStoreFive executeQuery:query];
    
    }
    

    Then you can just call it like so:

    HeartRateCalculator *commonClassTwo =[[HeartRateCalculator alloc] init];
    
    dispatch_group_t group = dispatch_group_create();
    
    [commonClassTwo calculateData:0 group:group];
    [commonClassTwo calculateData:-1 group:group];
    [commonClassTwo calculateData:-2 group:group];
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // called when all tasks are finished.
        // update UI
    });
    

    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:

    Queries run on an anonymous background queue.

    Therefore your tasks are already asynchronous.