iosnsurlsessiondatataskbackground-fetch

How do I implement background fetch using a data task?


I'm trying to implement background fetch in an iOS app. In the official documentation is stated:

"You don’t have to do all background network activity with background sessions ... ... . Apps that declare appropriate background modes can use default URL sessions and data tasks, just as if they were in the foreground."

Moreover, in this WWDC 2014 video at 51:50 between the best practices is stated:

"One common mistake that we think some people have made in the past is assuming that when running in the background to handle things like a background fetch update... ...they have been required to use background session. Now, using background upload or download... ...works really well, but in particular for large downloads. When you're running for a background fetch update ... you have about 30 to 60 seconds to run in the background" [before the application gets suspended] "If you have same small networking task that could finish within this time it is perfectly ok to do this in an in-process or default NSURLSession ... ..."

I am exactly in the situation of a small networking task: I want to communicate with a rest api on our server, to post same data and get back the data response. I'm creating a single default session and executing one or two tasks, synchronising them using a dispatch group, like in this example:

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"AppDelegate application:performFetchWithCompletionHandler:");
    BOOL thereIsALoggedInUser = [self thereIsALoggedInUser];
    if (!(thereIsALoggedInUser && [application isProtectedDataAvailable])){
        completionHandler(UIBackgroundFetchResultNoData);
        NSLog(@"completionHandler(UIBackgroundFetchResultNoData): thereIsALoggedInUser: %@, isProtectedDataAvailable: %@",
          thereIsALoggedInUser ? @"YES" : @"NO",
          [application isProtectedDataAvailable] ? @"YES" : @"NO");
        return;
    }

    NSArray<NSString*> *objectsIWantToSend = [self getObjectsIWantToSend];    
    if (!objectsIWantToSend) {
        [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
        completionHandler(UIBackgroundFetchResultNoData);
        NSLog(@"No post data");
    } else {
        [self sendToServer:objectsIWantToSend usingCompletionHandler:completionHandler];
    }
}  

-(void) sendToServer:(NSArray<NSString*> *)objectsArray usingCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"AppDelegate - sendToServer:usingCompletionHandler:");
    __block BOOL thereWasAnError = NO;
    __block BOOL thereWasAnUnsuccessfullHttpStatusCode = NO;    

    dispatch_group_t sendTransactionsGroup = dispatch_group_create();

    NSURLSession *postSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

    for (NSString *objectToPost in objectsArray) {

        dispatch_group_enter(sendTransactionsGroup);

        NSMutableURLRequest *request = [NSMutableURLRequest new];
        [request setURL:[NSURL URLWithString:@"restApiUrl"]];
        [request setHTTPMethod:@"POST"];
        request = [Util setStandardContenttypeAuthorizationAndAcceptlanguageOnRequest:request];
        [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:@{ @"appData": objectToPost } options:kNilOptions error:nil]];

        NSURLSessionDataTask *dataTask = [postSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){

            NSInteger statusCode = -1;
            if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                statusCode = [(NSHTTPURLResponse *)response statusCode];
            }

            if (error) {
                NSLog(@"Error");
                thereWasAnError = YES;
            } else if (statusCode != 201) {
                NSLog(@"Error: http status code: %d", httpResponse.statusCode);
                thereWasAnUnsuccessfullHttpStatusCode = YES;
            } else {
                [self parseAndStoreData:data];
            }

            dispatch_group_leave(sendTransactionsGroup);
        }];

        [dataTask resume];
    }

    dispatch_group_notify(sendTransactionsGroup, dispatch_get_main_queue(),^{
        if (thereWasAnError) {
            completionHandler(UIBackgroundFetchResultFailed);
        } else if (thereWasAnUnsuccessfullHttpStatusCode) {
            [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
            completionHandler(UIBackgroundFetchResultNoData);
        } else {
            completionHandler(UIBackgroundFetchResultNewData);
        }
    });
}

In my application:didFinishLaunchingWithOptions: method I am setting the BackgroundFetchInterval to the MinimumInterval using

[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

I am not force-quitting the app on the device because I know that the system in that case won't call the AppDelegate application:performFetchWithCompletionHandler: method until a user restarts the app

I'm using several test cases with several combinations of devices locked/unlocked, charging/not charging, all under a wifi network because they've got no sims

When I run the aforementioned kind of code using Xcode -> Debug -> Simulate background fetch or using a build scheme with the "launch due to a background fetch event" option selected all goes well: the server gets my post request, the app correctly has the server response back, and the dispatch_group_notify block gets executed. Testing the code through an actual device I get instead no result, not even the call to the server


Solution

  • Either background fetch is not being initiated or something is going wrong during the background fetch. We need to do a little debugging to identify what the issue is.

    So, a couple of observations:


    As an aside, unrelated to your question at hand, you really should check to make sure that the response was a NSHTTPURLResponse. In the unlikely event it wasn’t, do you want your app to crash when you try to retrieve the statusCode? Thus:

    os_log(log, "%{public}s", __FUNCTION__);
    
    __block BOOL thereWasAnError = NO;
    __block BOOL thereWasAnUnsuccessfullHttpStatusCode = NO;
    
    dispatch_group_t sendTransactionsGroup = dispatch_group_create();
    
    NSURLSession *postSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    
    for (NSString *objectToPost in objectArray) {
        dispatch_group_enter(sendTransactionsGroup);
    
        NSMutableURLRequest *request = [NSMutableURLRequest new];
        [request setURL:[NSURL URLWithString:@"restApiUrl"]];
        [request setHTTPMethod:@"POST"];
        request = [Util setStandardContenttypeAuthorizationAndAcceptlanguageOnRequest:request];
        [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:@{ @"appData": objectToPost } options:kNilOptions error:nil]];
    
        NSURLSessionDataTask *dataTask = [postSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
    
            NSInteger statusCode = -1;
            if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                statusCode = [(NSHTTPURLResponse *)response statusCode];
            }
    
            if (error) {
                os_log(log, "%{public}s: Error: %{public}@", __FUNCTION__, error);
                thereWasAnError = YES;
            } else if (statusCode < 200 || statusCode > 299) {
                os_log(log, "%{public}s: Status code: %ld", __FUNCTION__, statusCode);
                thereWasAnUnsuccessfullHttpStatusCode = YES;
            } else {
                [self parseAndStoreData:data];
            }
    
            dispatch_group_leave(sendTransactionsGroup);
        }];
    
        [dataTask resume];
    }
    
    dispatch_group_notify(sendTransactionsGroup, dispatch_get_main_queue(),^{
        if (thereWasAnError) {
            completionHandler(UIBackgroundFetchResultFailed);
        } else if (thereWasAnUnsuccessfullHttpStatusCode) {
            // [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
            completionHandler(UIBackgroundFetchResultNoData);
            return;
        } else {
            completionHandler(UIBackgroundFetchResultNewData);
        }
    });
    

    I personally also wouldn’t bother changing the fetch interval on failure, though that’s up to you. The OS will consider that, itself, when determining when to next initiate background fetch to your app.