How to get server response data in NSURLSession without completion block
Asked Answered
J

1

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.

Jesusitajet answered 19/5, 2014 at 6:31 Comment(5)
You can use NSURLConnection or AFNetworkingBuff
Thanx for reply @Morpheus, But does that support background upload?Jesusitajet
Can you suggest some code or tutorial link.Jesusitajet
https://mcmap.net/q/355734/-asynchronous-upload-with-nsurlsession-will-not-work-but-synchronous-nsurlconnection-does go through hereBuff
FYI, I enumerate a few of the considerations when doing background upload task here: https://mcmap.net/q/355735/-how-to-get-backgroundsession-nsurlsessionuploadtask-responseRf
R
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.

Rf answered 19/5, 2014 at 12:49 Comment(16)
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?Spector
URL Loading System Programming Guide 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)."Rf
Yeach, I read it before, but in terms of web service client API, I don't know how to group tasks together.Spector
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).Rf
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.Spector
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!!!Powers
@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.Rf
@Rf 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?Audiophile
@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).Rf
@Rf 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 [email protected].Audiophile
@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 https://mcmap.net/q/355736/-synchronizing-read-write-access-to-an-instance-variable-for-high-performance-in-ios. Having said that, the difference is unlikely to be material in this case, so synchronize it however you want.Rf
@Rf 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: #34760765. Thanks in advanceHopeless
Hi @Rf 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.Shilling
@Shilling - 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:) are quite explicit that completion handler must be called on the main thread.Rf
Hi @Rf 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.Displace
@Displace - 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.Rf

© 2022 - 2024 — McMap. All rights reserved.