50

I am using NSURLSession for background image uploading. And according to uploaded image my server gives me response and I do change in my app accordingly. But I can't get my server response when my app uploading image in background because there is no completion block.

Is there way to get response without using completion block in NSURLUploadTask?

Here is my code :

 self.uploadTask = [self.session uploadTaskWithRequest:request fromData:body completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
            NSString *returnString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSLog(@"returnString : %@",returnString);
            NSLog(@"error : %@",error);
        }];
 [self.uploadTask resume];

But i got this error..

Terminating app due to uncaught exception 'NSGenericException', reason: 'Completion handler blocks are not supported in background sessions. Use a delegate instead.'

But if I can't use completion handler than how should I get the server response. It says use delegate but I can't find any delegate method which can gives me server response.

rmaddy
  • 314,917
  • 42
  • 532
  • 579

1 Answers1

80

A couple of thoughts:

First, instantiate your session with a delegate, because background sessions must have a delegate:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kSessionIdentifier];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];

Second, instantiate your NSURLSessionUploadTask without a completion handler, because tasks added to a background session cannot use completion blocks. Also note, I'm using a file URL rather than a NSData:

NSURLSessionTask *task = [self.session uploadTaskWithRequest:request fromFile:fileURL];
[task resume];

Third, implement the relevant delegate methods. At a minimum, that might look like:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSMutableData *responseData = self.responsesData[@(dataTask.taskIdentifier)];
    if (!responseData) {
        responseData = [NSMutableData dataWithData:data];
        self.responsesData[@(dataTask.taskIdentifier)] = responseData;
    } else {
        [responseData appendData:data];
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        NSLog(@"%@ failed: %@", task.originalRequest.URL, error);
    }

    NSMutableData *responseData = self.responsesData[@(task.taskIdentifier)];

    if (responseData) {
        // my response is JSON; I don't know what yours is, though this handles both

        NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
        if (response) {
            NSLog(@"response = %@", response);
        } else {
            NSLog(@"responseData = %@", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);
        }

        [self.responsesData removeObjectForKey:@(task.taskIdentifier)];
    } else {
        NSLog(@"responseData is nil");
    }
}

Note, the above is taking advantage of a previously instantiated NSMutableDictionary called responsesData (because, much to my chagrin, these "task" delegate methods are done at the "session" level).

Finally, you want to make sure to define a property to store the completionHandler provided by handleEventsForBackgroundURLSession:

@property (nonatomic, copy) void (^backgroundSessionCompletionHandler)(void);

And obviously, have your app delegate respond to handleEventsForBackgroundURLSession, saving the completionHandler, which will be used below in the URLSessionDidFinishEventsForBackgroundURLSession method.

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
    // This instantiates the `NSURLSession` and saves the completionHandler. 
    // I happen to be doing this in my session manager, but you can do this any
    // way you want.

    [SessionManager sharedManager].backgroundSessionCompletionHandler = completionHandler;
}

And then make sure your NSURLSessionDelegate calls this handler on the main thread when the background session is done:

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    if (self.backgroundSessionCompletionHandler) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.backgroundSessionCompletionHandler();
            self.backgroundSessionCompletionHandler = nil;
        });
    }
}

This is only called if some of the uploads finished in the background.

There are a few moving parts, as you can see, but that's basically what's entailed.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Thank you Rob. This really shed some light on how NSURLsession is used. Could you please advise how you decide which tasks should be ran within one NSURLSession object and which ones should be separated? – Yevhen Dubinin Oct 28 '14 at 09:30
  • 1
    [URL Loading System Programming Guide](https://developer.apple.com/library/ios/Documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/UsingNSURLSession.html) says "To use the `NSURLSession` API, your app creates a series of sessions, each of which coordinates a group of related data transfer tasks. For example, if you are writing a web browser, your app might create one session per tab or window. Within each session, your app adds a series of tasks, each of which represents a request for a specific URL (and for any follow-on URLs if the original URL returned an HTTP redirect)." – Rob Oct 28 '14 at 13:12
  • Yeach, I read it before, but in terms of web service client API, I don't know how to group tasks together. – Yevhen Dubinin Oct 28 '14 at 13:15
  • Group them together however you want. It's entirely up to you. It really doesn't matter. Honestly, I generally have a single session manager for the entire app (and, if I need it, another separate background session manager for requests that should continue after my app terminates). – Rob Oct 28 '14 at 13:22
  • I think, I found where I can use single NSURLSession for multiple tasks: Remove Image class w/ ability to load itself from URL. So, I will use static NSURLSession var w/ default configuration and will submit tasks to it. – Yevhen Dubinin Nov 14 '14 at 01:26
  • 1
    Wait wait!!!!! This doesn't work. This if for a background upload task: This function is not called when the app is in the background for a background upload task!! And this is where you are getting the response!!! - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data Please help!!! – Andrew Paul Simmons Apr 02 '15 at 18:00
  • @AndrewPaulSimmons Are you sure about that? I just tested it again, and it works fine. This isn't called until the app is brought back to life, but it is called. `didReceiveResponse` is not, but `didReceiveData` is. – Rob Apr 02 '15 at 20:46
  • @Rob Great answer. I was wondering if we would need any locking around the responsesData which is a mutable class. In Objective-C mutable classes are not thread safe. If multiple tasks in the same session try to modify the responsesData in the delegate callbacks from different threads, won't the app crash without locks? – SayeedHussain May 30 '15 at 12:32
  • @paranoidcoder Yes, it would be prudent to synchronize `responsesData`. You could use locks to do that, though you could use other synchronization techniques, too (e.g., I personally would use reader-writer dispatch queue rather than lock). – Rob May 30 '15 at 12:45
  • @Rob Can you please elaborate a little on why "I personally would use reader-writer dispatch queue rather than lock". If not here, you can write to me at sayeedhussain19@gmail.com. – SayeedHussain Jun 01 '15 at 05:52
  • @paranoidcoder The reader-writer pattern is faster than typical locks (esp in cases like this where, if there is any concurrent access at all, for large downloads it's more likely to happen during reads than during writes). See http://stackoverflow.com/a/20939025/1271826. Having said that, the difference is unlikely to be material in this case, so synchronize it however you want. – Rob Jun 01 '15 at 12:15
  • @Rob thanks for your clarification on process, but seems I can't receive data when I'm trying to restore app: I start my upload process, than kill app. After I restart it I get into `didReceiveData`, but data is nil. Are there any way how I can get data for that task? Here is more details on my question: http://stackoverflow.com/questions/34760265/nsurlsessionuploadtask-get-reponse-data. Thanks in advance – user1284151 Jan 15 '16 at 09:20
  • Hi @Rob I can't get why you are calling the completion handler in URLSessionDidFinishEventsForBackgroundURLSession on the main queue. We are talking about uploading something in BG, so the main thread shouldn't even be "up and running", right? Can you explain this point? Thank you. – superpuccio Apr 11 '19 at 14:56
  • 1
    @superpuccio - Very different issues: “Background” thread/queue has nothing to do with “background” `URLSession` nor with “background” execution state of app. When background `URLSession` tasks finish, the app fires up app in “background mode” (i.e. as opposed to foreground mode), but the app still has a main thread and background threads. And docs for [`urlSessionDidFinishEvents(forBackgroundURLSession:)`](https://developer.apple.com/documentation/foundation/urlsessiondelegate/1617185-urlsessiondidfinishevents) are quite explicit that completion handler must be called on the main thread. – Rob Apr 11 '19 at 15:45
  • Hi @Rob While using [self.session uploadTaskWithRequest:request fromFile:fileURL]; What should I write in file at fileURL? I am doing let tempDir = FileManager.default.temporaryDirectory let fileURL = tempDir.appendingPathComponent(uniqueID) try imageData.write(to: fileURL) But it doesn't work in my case. I am trying to upload an image to amazon s3 presigned URL. If I use the same S3 url with POSTMAN or Alamofire and pass image data it works but it is not working with URLSession. – ViruMax Jan 03 '20 at 09:26
  • 1
    @ViruMax - I’d suggest you first write a rendition of this that uses a foreground `URLSession` with a data task and make sure the problem isn’t just the basics of how your request was formatted. Regardless, this is beyond the scope of what we can tackle in comments here and you should post a question showing your code and showing the curl command. – Rob Jan 03 '20 at 17:30