CMMotionActivityManager, NSOperationQueues and Async Behaviour

I’ve just deleted a couple of articles from the blog for the first time in five years because, having spent quite a few hours going over the example code I’d posted, I realised that it was rubbish.

There is a characteristic of some of the CMMotionActivityManager methods which gets you into some serious threading detail: they return results to an NSOperationsQueue asynchronously. What I was doing with the simpler of the two, queryStepCountStartingFrom, was to wrap a looping call to the inner results queue in an outer queue. So: I have an ‘inner’ results queue, which is being called as a parameter to the queryStepCountStartingFrom method, effectively adding results which return asynchronously. I then force this to be a prerequisite by making the entire loop the first add operation on an outer queue. When the loop is done I make the call to the Core Plot charting methods, because all of the async data will have returned before the charting operation is added. Right?

Wrong.

It’s pure chance that the async operations return in time. The queue will merrily accept new operations regardless of whether the previous tasks have completed.

These problems emerged when I tried to do the same thing with the slightly more complex queryActivityStartingFromDate method, which returns a richer data set, and so the async calls take significantly longer to return. I’ve been round the houses on this: the nested queues approach didn’t work, nor did a GCD async dispatch queue wrapped around the loop. I also tried messing around with adding dependencies between nested operations. No joy.

The fundamental problem [so far as I can see] is that the first port of call for identifying the end of some queue activity, waitUntilAllOperationsAreFinished, is a synchronous animal and therefore has no purview on the results coming back asynchronously on the queue.

What I really want to avoid is what I’ve done elsewhere when I’ve understood the problem less clearly, which is to mess around with timers. This is a pretty fragile approach.

Googling for this has led me to the conclusion that the ‘right’ way – i.e., to reliably detect the async results coming back – is to do something pretty low level with either threading or GCD.

As a compromise, what I’ve come up with is to set an observer on the queue, which I first serialise [maxConcurrentOperationCount = 1]. In the observeValueForKeyPath method, I can test for the queue being empty, and increment a counter. When the counter hits the threshold for the number of day’s worth of data / loops round the CMMotionActivityManager methods for [i.e., 7], I am done. It ain’t pretty but it works.
Today.

FWIW, here’s the code:

- (void) getDailyActivityTotals
{
    NSLog(@"Entering getDailyTotals");
    // activityQueue is declared as a property as we need to refer to it in the observer method:
    activityQueue.maxConcurrentOperationCount = 1;
    activityQueue.name =@ "activityQueue";
    [activityQueue addObserver:self forKeyPath:@"operations" options:0 context:NULL];
    
    NSLog(@"starting to loop in calActivityOpn");
    for (int dayOffset = 6; dayOffset >=0; dayOffset--)
    {
        NSMutableArray *dailyResultsTmpArray = [[NSMutableArray alloc] init];
        // startAndEndDates is a method which uses NSCalendar and NSDateComponents to
        // create some NSDate objects based on an offset: 24 hour periods - start and end dates:
        dailyResultsTmpArray = [self startAndEndDates:dayOffset];
        [motionActivity queryActivityStartingFromDate:dailyResultsTmpArray[1]
                                               toDate:dailyResultsTmpArray[0]
                                              toQueue:activityQueue
                                          withHandler:^(NSArray *activities, NSError *error)
        {
            NSInteger walkingCount = 0;
            NSInteger runningCount = 0;
            NSInteger carCount = 0;
            NSInteger staticCount = 0;
            NSInteger actionItemCount = 0;
            // This returns a lot of data, so we are just counting the number of events
            // per activity category:
            for (CMMotionActivity *actItem in activities)
            {
                if (actItem.walking == 1)
                    walkingCount++;
                if (actItem.running == 1)
                    runningCount++;
                if (actItem.automotive == 1)
                    carCount++;
                if (actItem.stationary == 1)
                    staticCount++;
                actionItemCount++;
            }
            //NSLog(@"walking: %i", walkingCount);
            DailyActivityObject *todaysResults = [[DailyActivityObject alloc] initWithNumWalkCount:walkingCount numRun:runningCount nummAuto:carCount numStatic:staticCount numAction:actionItemCount forDay:dailyResultsTmpArray[0]];
            [dailyResults addObject:todaysResults];
            NSLog(@"adding dailyResults");
                
        }];

    }
}

and the KVO method:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                         change:(NSDictionary *)change context:(void *)context
{
    if (object == activityQueue && [keyPath isEqualToString:@"operations"]) 
    {
        // if the queue is empty - remember we have serialised:
        if (activityQueue.operationCount == 0)
        {
            oberserverCount ++;
            NSLog(@"queue has completed %i iterations", oberserverCount);
            if (oberserverCount == 7)
            {
                // we have all the charting data so....
                [activityQueue addOperationWithBlock:^{
                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                        [self plotCompoundChart:dailyResults];
                    }];
                }];
            }
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object
                               change:change context:context];
    }
}