Search code examples
iosobjective-cmultitaskingnsurlsessiondownloadtaskbackground-thread

NSURLSessionDataTask not called after NSURLSessionDownloadTask in the same NSURLSession in iOS background


My iOS app works in the following way

  1. I have an array of audio file names, I check whether the file is present. If it's present it starts playing it. Once finished I start playing the next audio.

  2. If the file is not present I make a NSURLSessionDataTask POST request which returns me a string which is the URL of the file to be downloaded.(This URL is valid for one minute).

  3. Once I receive the URL, I make a NSURLSessionDownloadTask request and download the file, save the file and play the audio.

This entire process works perfectly fine when the app is in foreground. Also works perfectly fine when all the audios are present and the app is running in background.

The problem comes when the audio files are not present and the app is running in background

My code : BackgroundFetchManager.h

@interface BackgroundFetchManager : NSObject <NSURLSessionDownloadDelegate,NSURLSessionDataDelegate>

@property bool isDownloading;
@property NSURLSessionDownloadTask *download;
@property NSURLSessionDataTask *dataDownload;
@property (nonatomic, strong)NSURLSession *backgroundSession;
@property NSMutableArray *downloadQueue;
@property FileAccessManager *fileAccessManager;

@end

BackgroundFetchManager.m

Initialization function

-(id) init
{
    self = [super init];
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"testConfiguration"];
    config.sessionSendsLaunchEvents = YES;
    config.discretionary = YES;
    self.backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    download = [NSURLSessionDownloadTask new];
    dataDownload = [NSURLSessionDataTask new];
    return self;
} 

Data task

dataDownload = [self.backgroundSession dataTaskWithRequest:request];
[dataDownload setTaskDescription:@"Data task"];

Delegates of dataDownload (NSURLSessionDataTask)

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{

    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data
{
    NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSString *url = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"This is the response received %@",url);

//download is the NSURLSessionDownloadTask
    download = [self.backgroundSession downloadTaskWithURL:[NSURL URLWithString:url]];
    [download resume];

}

Delegates of download (NSURLSessionDownloadTask)

These are the ones which are not called when the app is in background

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    AppDelegate *a = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSURL *destinationURL = [fileAccessManager saveFile:a.downloadQueue[0] fromLocation:location]; // save file code
}


- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    // audio playing stuff

    if (![task.taskDescription isEqualToString:@"Data task"]) {
        NSLog(@"This is did complete with error %@", task );
        AppDelegate *a = (AppDelegate *)[[UIApplication sharedApplication] delegate];
        if ([a.downloadQueue[0] isEqualToString:a.audioToBePlayed]) {
            NSDictionary* userInfo = @{@"file": a.downloadQueue[0]};
            [[NSNotificationCenter defaultCenter] postNotificationName:@"playThisStuff" object:nil userInfo:userInfo];


        }
//starts next download
        [a pop];
        isDownloading = false;
        if ([a.downloadQueue count] > 0) {
            isDownloading = true;
            [self downloadFile:a.downloadQueue[0]];
        }
    }        
}

So in a nutshell, the downloadtask is not called after the datatask has finished. As much as I know, NSURLSession when configured to backgroundsession, the OS takes up the responsiblity of download, so ideally it should work. Am I missing something ?


Solution

  • The problem stems from the fact that iOS won't let you start a new task while the app is in the background. I believe that this is done to prevent apps from continuously firing off new tasks and keeping the app alive indefinitely.

    Background tasks are generally suitable for gracefully handling your app transitioning from foreground to background while a task is in progress. It's the reason you're seeing the first task complete, but not the second.

    One workaround would be to request background execution time from the OS when you fire off the initial data task:

    UIBackgroundTaskIdentifier backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
    
    }];
    

    That should at least give your app a few minutes of background time to initiate the second task. Be sure to call -endBackgroundTask: after you call -resume on the second task.

    Unfortunately, this cannot guarantee that all of your files will download. If you find that you're running out of background time, you may be better off using Silent Push Notifications to wake your app and download periodically.