Running Some Code Multiple Times Asynchronously With Different Variables

218 views Asked by At

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

// } }

3

There are 3 answers

8
Hamish On BEST ANSWER

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.

0
Jeremiah On

You can make seperate classes instead and run them all at the same time.

HeartRateCalculator *commonClassOne =[[HeartRateCalculator alloc] init];
HeartRateCalculator *commonClassTwo =[[HeartRateCalculator alloc] init];
HeartRateCalculator *commonClassThree =[[HeartRateCalculator alloc] init];
[commonClassOne calculateData:0];
[commonClassTwo calculateData:-1];
[commonClassThree calculateData:-2];

This way you don't have to duplicate the code but they won't overwrite values in the same class.

6
Warif Akhand Rishi On

Use block

[commonClassTwo calculateData:0 withCompletionBlock:^{
    [commonClassTwo calculateData:-1 withCompletionBlock:^{
        [commonClassTwo calculateData:-2 withCompletionBlock:^{
            NSLog(@"All 3 calculation done");
        }];
    }];
}];

- (void)calculateData:(NSInteger)monthsBack withCompletionBlock:(void(^)())completionBlock {
    //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
        completionBlock();
    }
    [healthStoreFive executeQuery:query];
}

Update

For async call, have a counter an integer. Initial value will be the number of async calls you want to have. (here 3)
In the completionBlock of each call, decrement the counter and check if all calls are completed or not.
if completed update the UI.

[commonClassTwo calculateData:0 withCompletionBlock:^{
    // counter--;
    // [self updateUI];
}];

[commonClassTwo calculateData:-1 withCompletionBlock:^{
    // counter--;
    // [self updateUI];
}];

[commonClassTwo calculateData:-2 withCompletionBlock:^{
    // counter--;
    // [self updateUI];
}];


- (void)updateUI {
    // if counter == 0 {
    //     dispatch_async(dispatch_get_main_queue(), ^{
    //         update UI
    //     });
    // }
}