NSURLSessionUploadTask background question.

Hi folks,


Been working with background uploads. However, I'm not sure about some cases or what is the best way to handle it.


For example: I start an upload task, I kill the app by swiping it off. My understanding is that the task can finish in background, reopen the app, and resume.


The two parts I am not too sure is how to:

1) Upon opening the app getting the running task and reconnecting them instead of recreating new once.

2) Upon app getting relaunched from task completion, get it's response or progress if this happens when app was dead.


If I recreate an NSURLSession with the same identifier I started the tasks before kill the app, will apple api's let me get all the tasks that are still running when I call "getTasksWithCompletionHandler"? And if yes, how do I get them reconnected to my delegate?


What I have is AppDelegate: handleEventsForBackgroundURLSession

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
  completionHandler:(void (^)(void))completionHandler {
    NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
    NSURLSession* session = [NSURLSession sessionWithConfiguration:config delegate:taskProcessorSingleton delegateQueue:nil];
    [session getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
        //TODO: Can I get responseData for tasks here? Or is this a complete wrong way of looking at it?
       // should I expect that delegate here will start getting calls such as "receivedResponse" and "didCompleteWithError"?
        completionHandler();
}

Here is how I make tasks in a singleton class and handle responses ... all works well in foreground:

// make session - this is done once in a singleton.
NSString * sessionName = @"backgroundSessionImageUpload";
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionName];
configuration.allowsCellularAccess = YES;
configuration.discretionary = NO;
configuration.sessionSendsLaunchEvents = YES;
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];

// make request and task - this is done per upload
NSURLRequest * request = [self createRequest];

NSURLSessionUploadTask* task = [self.session uploadTaskWithRequest:request fromFile:url];
task.taskDescription = [NSString stringWithFormat:@"%ld", self.uploadID]; // so I know which one is which.

// store in local array and start.
[self.tasks addObject:task];
[task resume];



- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
// handle completion or error... when all is done.
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
// response data here...
}


Thank you very much,


Chris

Accepted Reply

Lines 8 through 11 of listing 2 are weird:

  • If the task is in

    NSURLSessionTaskStateRunning
    there’s no need to call
    -resume
    on it.
  • The only way that a task can get into

    NSURLSessionTaskStateSuspended
    is if you call
    -suspend
    on it (or if you didn’t resume it in the first place). Neither of these seem likely.

Personally I’d delete this code, replacing it with an assert so that you learn if there’s something totally unexpected happening.

With regards listing 5 (well, you didn’t give it a number but it’s the listing after 4 :-), you only get the data callback if the server sends you back data. You should do your test for a transport error (via the

error
parameter) and your test for a server-side error (via the
statusCode
property) in your
-URLSession:task:didCompleteWithError:
delegate callback.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

I start an upload task, I kill the app by swiping it off. My understanding is that the task can finish in background, reopen the app, and resume.

This is, alas, not correct. When you kill an app via the multitasking UI, the system interprets that as a strong indication from the user that the app should do no more work in the background, and that includes

NSURLSession
background tasks. This is a common source of confusion, so I updated my Testing Background Session Code pinned post to cover that case.

Coming back to your programming question, there are two situations to consider:

  • Resume in the background

  • Relaunch in the background

When you are resumed your app hasn’t been terminated, so all of your in-memory state is preserved. Things proceed as follows:

  1. The system calls

    -application:handleEventsForBackgroundURLSession:completionHandler:
    .
  2. You save the completion handler.

  3. The background session will start calling delegate methods and you should handle them in the usual way.

  4. When it’s done the background session will call

    -URLSessionDidFinishEventsForBackgroundURLSession:
    at which point you call the completion handler you saved in step 2
  5. The system suspends your app.

The relaunch case is more complex because your app has been terminated and thus has lost its in memory state. There are two ways to regenerate this state:

  • You can persist it yourself (A)

  • You can rebuild it from the session using

    -getAllTasksWithCompletionHandler:
    (B)

Both of these have their drawbacks. With A you will probably end up needing to sync your state with the session anyway because there are situations where the session can drop state (for example, a restore from backup). With B you have to deal with a tricky race condition.

For the moment let’s consider B. Things proceed as follows:

  1. The system relaunches your app and calls

    -application:willFinishLaunchingWithOptions:
    . Your implementation re-creates your background session and calls
    -getAllTasksWithCompletionHandler:
    .

    Note It’s possible to avoid doing this here and instead do it lazily in step 3. However, I prefer this approach because it seems more direct.

  2. Your completion handler runs and you rebuild your in-memory state based on that.

  3. The system calls

    -application:handleEventsForBackgroundURLSession:completionHandler:
    .
  4. You save the completion handler.

  5. The background session will start calling delegate methods and you should handle them in the usual way.

  6. When it’s done the background session will call

    -URLSessionDidFinishEventsForBackgroundURLSession:
    at which point you call the completion handler you saved in step 2
  7. The system suspends your app.

The tricky part is the race condition between steps 2 and step 5. It’s possible you could get a delegate callback for a task before your

-getAllTasksWithCompletionHandler:
completion handler has run and has rebuilt your in-memory state. You can deal with that by having your delegate callbacks incrementally rebuild the state of any task they learn about.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks Eskimo,


We got it working pretty well, except for occasional "getting stuck", and it's probably my code where app thinks it's uploading while it actually isn't. Is there a hole in this? Can you please advise?


Short summary:

We create session (one per upload), session name contains uploadID in it to string parse:

When we create upload task by checking if session has any pending tasks, and if yes, we reuse it, if not, we make new uploadTask.

When we get delegate callback completed, we invalidate session.

When session becomes invalid, we notify app of success or failure.


What we noticed is that sometimes we can get a task that starts with "completed", and there might not be a delegate callback at any point happening. No "dataReceived", and no "didCompleteWithError". These just don't happen.


//1 - create session

- (NSURLSession*)createSession {

    NSString * sessionName = [NSString stringWithFormat:@"%@_%ld", @"backgroundSessionImageUpload", self.uploadID];
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionName];
    configuration.allowsCellularAccess = YES;
    configuration.discretionary = NO;
    configuration.sessionSendsLaunchEvents = NO;
    return [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
}

// 2 - create task

[self.session getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks) {
        
            if (tasks.count > 0) {
            
                NSURLSessionUploadTask* task = tasks[0];
                self.task = task;
            
                if (task.state == NSURLSessionTaskStateRunning
                    || task.state == NSURLSessionTaskStateSuspended) {
                    [task resume];
                }

               // sometimes state == NSURLSessionTaskStateCompleted/NSURLSessionTaskStateCancelled... don't know what to do here?
                return;
            }
        
            NSURLRequest * request = [self buildRequest];
            NSURL* url = [NSURL fileURLWithPath:filePath];
            self.task = [self.session uploadTaskWithRequest:request fromFile:url];
}


// 3 - listen to finish - and invalidate session, because at times we get a callback that session became invalid.

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {

    [self.session finishTasksAndInvalidate];
}

// 4 - session invalid, notify app of success or failure.

- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error {

    if (self.responseDict == nil) {
     // notify app of failure.
  } else {
   // notify app of success.
  }
  // call completion handler from app delegate here...
}

// sometimes we don't get this call - but just get didCompleteWithError(nil error).

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {

    NSLog(@"didReceiveData");

    NSError *error;

    @try {
        NSHTTPURLResponse* resp = (NSHTTPURLResponse *)dataTask.response;
        self.statusCode = resp.statusCode;
  
        self.responseDict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
    } @catch(NSException* ex) {
        self.responseDict = nil;
    }
}

Lines 8 through 11 of listing 2 are weird:

  • If the task is in

    NSURLSessionTaskStateRunning
    there’s no need to call
    -resume
    on it.
  • The only way that a task can get into

    NSURLSessionTaskStateSuspended
    is if you call
    -suspend
    on it (or if you didn’t resume it in the first place). Neither of these seem likely.

Personally I’d delete this code, replacing it with an assert so that you learn if there’s something totally unexpected happening.

With regards listing 5 (well, you didn’t give it a number but it’s the listing after 4 :-), you only get the data callback if the server sends you back data. You should do your test for a transport error (via the

error
parameter) and your test for a server-side error (via the
statusCode
property) in your
-URLSession:task:didCompleteWithError:
delegate callback.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks Eskimo.


I'll make that change.