Search code examples
iosobjective-chealthkithksamplequery

NSInternalInconsistencyException when running background fetch with a loop


I have this code that is trying to do a background fetch for HealthKit data. The code works fine when I first run the app, but if I manually perform a background fetch (using the debug command), I get an exception thrown and an error that says reason: 'this request has been neutered - you can't call -sendResponse: twice nor after encoding it' and I'm not quite sure why.

Here is the code that fetches the data:

- (void)fetchNewDataWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

    if (!self.healthStore) {
        self.healthStore = [HKHealthStore new];
    }

    dataTypes = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNumber numberWithInt:1], HKQuantityTypeIdentifierStepCount,
                 [NSNumber numberWithInt:2], HKQuantityTypeIdentifierFlightsClimbed,
                 [NSNumber numberWithInt:3], HKQuantityTypeIdentifierDistanceWalkingRunning,
                 [NSNumber numberWithInt:4], HKQuantityTypeIdentifierDistanceCycling, nil];
    achievementData = [NSMutableDictionary new];

    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDate *startDate = [calendar startOfDayForDate:[NSDate date]];
    NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
    NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];

    for (NSString *key in dataTypes) {

        HKSampleType *sampleType = [HKSampleType quantityTypeForIdentifier:key];

        HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {

            if (!error) {

                if (!results) {

                    NSLog(@"No results were returned form query");

                    completionHandler(UIBackgroundFetchResultNoData);

                } else {

                    dispatch_async(dispatch_get_main_queue(), ^{

                        [self processNewDataWithResults:results andType:key];

                        completionHandler(UIBackgroundFetchResultNewData);

                    });

                }

            } else {

                NSLog(@"Error: %@ %@", error, [error userInfo]);

                completionHandler(UIBackgroundFetchResultFailed);

            }

        }];

        [self.healthStore executeQuery:query];

    }

}

Then I have a separate function that processes the data that you can see gets called when results are found. If you want I can paste it in here but it is pretty lengthy, not sure if it has anything to do with it.

I have tried putting breakpoints in to see when the completion handler is called but from what I can tell it is only getting called once, unless I am missing something silly here.

If anyone has any advice, please let me know :) Thanks!

EDIT Here is what the error message looks like:

2015-05-13 10:11:54.467 appName[379:169163] *** Assertion failure in -[UIFetchContentInBackgroundAction sendResponse:], /SourceCache/BaseBoard/BaseBoard-98.3/BaseBoard/BSAction.m:221
2015-05-13 10:11:54.470 appName[379:169163] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'this request has been neutered - you can't call -sendResponse: twice nor after encoding it'
*** First throw call stack:
(0x28ed4137 0x36e30c77 0x28ed400d 0x29bd2bc9 0x2dbde865 0x397ed5 0x2dbde7cf 0x2ca82a3d 0x39019b 0x390187 0x393e9d 0x28e99889 0x28e97fa9 0x28de39a1 0x28de37b3 0x305951a9 0x2c56e695 0xdff29 0x373d8aaf)
libc++abi.dylib: terminating with uncaught exception of type NSException

Solution

  • From looking at the code you posted, I do not see any other way than that the background fetch completion handler will be called exactly 4 times due to the for loop.

    The code paths in the resultHandlers of each instance of HKSampleQuery will end up at a completionHandler(UIBackgroundFetchResult...) call in any case and you are always instantiating four of them, so this is what the assertion 'you can't call -sendResponse: twice' is complaining about IMO.

    It should be easy to check if that is the problem by commenting out 3 of the queries from the dataTypes dictionary.

    Edit: as requested in the comments, here's a possible solution (..just off the top of my head, so take it with a grain of salt..):

    It (a) uses a semaphore lock to turn the asynchronous query result callback into a "synchronous" call & (b) uses a dispatch group to wait for all 4 queries to finish before executing the completion handler.

    // NOTE: This example assumes that the fetch has "new data" if any of the queries returned something
    //       Also it skips the 'NSError' part (which could work exactly like the 'result' var)
    
    // Define one or more block-global result variables for queries can put their end state into
    UIBackgroundFetchResult __block result = UIBackgroundFetchResultNoData;
    
    // Create a dispatch group that will manage all the concurrent queries
    dispatch_queue_t queue = dispatch_queue_create([@"my.query.queue" UTF8String], DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t queries = dispatch_group_create();
    
    // For each dataType dispatch a group block containing the query to run on the concurrent queue
    for (NSString *key in dataTypes) {
        dispatch_group_async(queries, queue, ^{
            // To work around the asynchronous callback, I'll use a lock to wait for the query to return their result, so..
    
            // ..like above, a block-global var will hold the result of the query
            BOOL __block success = NO;
    
            // ..create a one-time lock..
            dispatch_semaphore_t lock = dispatch_semaphore_create(0);
            HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:sampleType predicate:predicate limit:HKObjectQueryNoLimit sortDescriptors:nil resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
                success = YES; // ..or however you define success.. ;)
    
                dispatch_semaphore_signal(lock);    // ..open lock to signal result available..
            }];
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);   // ..wait for callback result lock to open..
    
            // ..determine & set result.
            if (success) {
                result = UIBackgroundFetchResultNewData;
            }
        });
    }
    
    // Schedule a final block to execute (on the main queue) when all the other group blocks have finished running
    dispatch_group_notify(queries, dispatch_get_main_queue(), ^{
        // Determine final result and call completion handler
        completionHandler(result);
    });