NSURLSession delegate vs. completionHandler - ios

I've always used completion handlers. With NSURLConnection and now with NSURLSession. It's led to my code being really untidy, especially I have request within request within request.
I wanted to try using delegates in NSURLSession to implement something I've done untidily with NSURLConnection.
So I created a NSURLSession, and created a dataTask:
NSURLSessionDataTask *dataTask = [overallSession dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(error == nil)
{
NSString * text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog(#"Data = %#",text);
}
}];
[dataTask resume];
Right now I have a completionHandler for the response, how would I switch to delegates to manage the response and data? And can I add another dataTask from the delegate of this one? Using the cookies that this dataTask created and placed into the session?

If you want to add a custom delegate class, you need to implement the NSURLSessionDataDelegate and NSURLSessionTaskDelegate protocols at the minimum.
With the methods:
NSURLSessionDataDelegate - get continuous status of your request
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
receivedData=nil; receivedData=[[NSMutableData alloc] init];
[receivedData setLength:0];
completionHandler(NSURLSessionResponseAllow);
}
NSURLSessionDataDelegate
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
[receivedData appendData:data];
}
NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
// Handle error
}
else {
NSDictionary* response=(NSDictionary*)[NSJSONSerialization JSONObjectWithData:receivedData options:kNilOptions error:&tempError];
// perform operations for the NSDictionary response
}
If you want to separate the delegate code (middle layer) from your calling class (generally its good practice to have separate class/layer for network calls), the delegate of NSURLSession has to be :-
NSURLSession *session=[NSURLSession sessionWithConfiguration:sessionConfig delegate:myCustomDelegateClass delegateQueue:nil];
Ref Links:
NSURLSession Class Reference
iOS NSURLSession Example (HTTP GET, POST, Background Downlads )
From NSURLConnection to NSURLSession

Related

Passing response to controller while converting NSURLConnection to NSURLSession

I have something like service API class which implements NSURLConnectionDelegate methods. I noticed that some of the methods of this are deprecated and Apple now suggests to use NSURLSession. I created this service API's own delegate which the calling class would implement and that would be executed when a response is received. I am looking for how I can do something similar while using NSURLSession.
My present stuff with NSURLConnection:
#class ServiceAPI;
#protocol ServiceAPIDelegate <NSObject>
- (void)getResponseData:(NSData *)responseData;
#end
#interface ServiceAPI : NSObject<NSURLCoDelegate>
- (void)httpServiceRequest:(NSMutableURLRequest *)serviceRequest;
#property (nonatomic, weak) id <ServiceAPIDelegate> delegate;
#end
In ServiceAPI implementation file:
- (void)httpServiceRequest:(NSMutableURLRequest *)serviceRequest {
//[[NSURLSession sharedSession] dataTaskWithRequest:serviceRequest] resume];
self.requestConnection = [NSURLConnection connectionWithRequest:serviceRequest delegate:self];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.delegate getResponseData:self.responseData sender:self];
}
Class making request and getting response:
#interface CallingController : UITableViewController<ServiceAPIDelegate>
#end
Implementation file CallingController:
- (void)getResponseData:(NSData *)responseData {
// Do something with response.
}
How do I let the calling class handle the response method just like NSURLConnection while using NSURLSession. While reading NSURLSession looks like the request and response are handled together using completion handler something like this:
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:#"https://itunes.apple.com/search?term=apple&media=software"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", json);
}];
I still want to have a service API class and my controller would just pass down the request to service API to make calls and once the response is back, it would be passed down to controller. How do I pass the response to the controller while using NSURLSession.
Something like this:
#class ServiceAPI;
#protocol ServiceAPIDelegate <NSObject>
- (void)getResponseData:(NSData *)responseData;
#end
#interface ServiceAPI : NSObject<NSURLSessionDelegate>
- (void)httpServiceRequest:(NSMutableURLRequest *)serviceRequest;
#property (nonatomic, weak) id <ServiceAPIDelegate> delegate;
#end
In Implementation
- (void)httpServiceRequest:(NSMutableURLRequest *)serviceRequest {
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *downloadTask =
[session dataTaskWithRequest:serviceRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
[self.delegate getResponseData:data sender:self];
}];
[downloadTask resume];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// no use
}
You can still achieve what you wish by initialising NSURLSessionDataTask without completion block. That way, it will call the delegate methods instead.
For example:
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:#"https://itunes.apple.com/search?term=apple&media=software"]];
[dataTask resume];
Implement NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data;
Also implement NSURLSessionTaskDelegate to know when the request is completed.
/* Sent as the last message related to a specific task. Error may be
* nil, which implies that no error occurred and this task is complete.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error;

Using NSURLProtocol with NSURLSession

My application uses NSURLConnection to communicate with server. We use https for communication. In order to handle authentication from all request in one place i used NSURLProtocol and handled authentication in delegates in that class. Now I have decided to use NSURLSession instead of NSURLConnection. I am trying do get NSURLProtocol working with NSURLSession
I created a task and used NSURLProtocol by
NSMutableURLRequest *sampleRequest = [[NSMutableURLRequest alloc]initWithURL:someURL];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = #[[CustomHTTPSProtocol class]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:checkInInfoRequest];
[task resume];
CustomHTTPSProtocol which is my NSURLProtocol class looks like this
static NSString * const CustomHTTPSProtocolHandledKey = #"CustomHTTPSProtocolHandledKey";
#interface CustomHTTPSProtocol () <NSURLSessionDataDelegate,NSURLSessionTaskDelegate,NSURLSessionDelegate>
#property (nonatomic, strong) NSURLSessionDataTask *connection;
#property (nonatomic, strong) NSMutableData *mutableData;
#end
#implementation CustomHTTPSProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([NSURLProtocol propertyForKey:CustomHTTPSProtocolHandledKey inRequest:request]) {
return NO;
}
return [[[[request URL]scheme]lowercaseString]isEqualToString:#"https"];
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
- (void) startLoading {
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:#YES forKey:CustomHTTPSProtocolHandledKey inRequest:newRequest];
NSURLSession*session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
self.connection = [session dataTaskWithRequest:newRequest];
[self.connection resume];
self.mutableData = [[NSMutableData alloc] init];
}
- (void) stopLoading {
[self.connection cancel];
self.mutableData = nil;
}
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSLog(#"challenge..%#",challenge.protectionSpace.authenticationMethod);
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
}
else {
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
NSLog(#"data ...%# ",data); //handle data here
[self.mutableData appendData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (!error) {
[self.client URLProtocolDidFinishLoading:self];
}
else {
NSLog(#"error ...%# ",error);
[self.client URLProtocol:self didFailWithError:error];
}
}
#end
Start loading is called and also authentication challenge is done but stop loading is called immediately after that.
Error code -999 "Cancelled" is returned after some time. didReceiveData is not called.
Note:NSURLProtocol and the Authentication Process worked fine with NSURLConnection.
What am I missing ?? My Questions are
Registering [NSURLProtocol registerClass:[CustomHTTPSProtocol class]]; worked fine with NSURLConnection but how to Resister for NSURLProtocol Globally with NSURLSession ?.
Why are the requests getting failed in NSURLProtocol(same URL and logic worked withURLConnection) with URLSession and how to get NSURLProtocol working with URLSession ?.
Kindly help me and let me know if you want any more details.
Registering a custom NSURLProtocol with NSUrlsession is same as the way you do with NSURLConnection.
NSURLSession Api's (NSURLSessionDataTask and other task classes) when used with custom NSUrlProtocol fails to handle HTTP Authentication Challenges properly.
This was working as expected on iOS 7.x and is broken in iOS 8.x.Raised a apple radar and my radar was closed saying it was a duplicate of another radar and I Still see the original radar is still open. So I believe this issue is not yet fixed by apple as of today.
Hope I am not too late with the answer :)
I know this is old but the problem you are having is in your didReceiveChallenge method. You are ending the method by calling
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
or
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
What you need to be doing instead is using the completion handler to send your results. It would look like this:
completionHandler(.UseCredential, NSURLCredential(forTrust: challenge.protectionSpace.serverTrust)
or
completionHandler(.PerformDefaultHandling, nil)
These are in Swift 2.0 but should translate nicely to Obj-C.
My vague recollection is that NSURLSession automatically uses any globally registered protocols. So you shouldn't need to register it again, but it also shouldn't hurt to do so.
I wonder if the URL request is getting copied, in which case your custom tagging might not work, and you might be seeing infinite recursion until it hits some internal limit. Have you tried setting a request header instead?

NSURLSessionDownloadTask Delegates not calling didWriteData method

I'm starting to implement a download method using NSURLSession and successfully downloaded different files from multiple request. But now I wanted to add a progress track, however the delegates for download progress is not being triggered.
Here is my code:
NSURLSessionConfiguration *defaultConfigObject = NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:self delegateQueue:nil];
NSURLSessionDownloadTask * downloadTask = [defaultSession downloadTaskWithRequest:request completionHandler:^(NSURL * __nullable location,
NSURLResponse * __nullable response, NSError * __nullable error) {
NSData *data = [NSData dataWithContentsOfURL:location];
[[NSFileManager defaultManager] createFileAtPath:docPath contents:data attributes:nil];
if ([[NSFileManager defaultManager] fileExistsAtPath:docPath]) {
NSDictionary *notificationDic = [[NSDictionary alloc] initWithObjectsAndKeys:docPath,#"docPath", item, #"item", nil];
[[NSNotificationCenter defaultCenter] postNotificationName: #"openFile" object:nil userInfo:notificationDic];
}
}];
[downloadTask resume];
I have the NSURLSessionDownloadDelegate on my header file.
I needed to use completion handler to be able to perform different tasks with the file.
Is there a way I can do it?
If you use downloadTaskWithRequest rendition without the completionHandler parameter, then the progress delegate methods will be called. Obviously, you'll have to move the code currently in the completionHandler block into the didFinishDownloadingToURL method. But if you do this, you'll see didWriteData called.
You will have to initiate your download with:
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;
And implement the delegate method for your progress:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
And since you need to perform various tasks when finished, you should also implement this delegate method:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;
Essentially, the completion handler routines are "convenience" routines to quickly perform the task and then when finished, perform the completion handler. But they don't call the other delegate routines.
In my case the problem was I conform my class with URLSessionDelegate instead of URLSessionDownloadDelegate. Even if I was implementing the URLSessionDownloadDelegate methods.

How can I receive my data in pieces with using NSURLConnection's sendAsynchronousRequest method?

When I send a synchronous request with NSURLConncetion
[NSURLConnection initWithRequest:myRequest delegate:self];
I can receive my downloaded data in pieces with the following method
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.videoData appendData:data];
NSLog(#"APPENDING DATA %#",data);
}
The advantage of this is that I can write my data directly to a file, and limit ram usage when downloading large files.
When I send an asynchronous request, how can I receive my data in pieces? The only place I see the data given back to me is in the completion handler of the request.
[NSURLConnection sendAsynchronousRequest:videoRequest
queue:downloadQueue
completionHandler:^(NSURLResponse* response, NSData* data, NSError* error){
NSLog(#"All data is given here!");
}];
Is there any solution to this problem? I'm downloading large files in a view controller and want to continue downloading them if the view controller gets dismissed. The problem is that I'm going to use too much memory if I receive all my data at once when downloading large files.
The only method in NSURLConnection which is synchronous is + sendSynchronousRequest:returningResponse:error:
The following methods are all asynchronous
+ connectionWithRequest:delegate:
– initWithRequest:delegate:
– initWithRequest:delegate:startImmediately:
+ sendAsynchronousRequest:queue:completionHandler:
– start
So the code [NSURLConnection initWithRequest:myRequest delegate:self]; itself is an asynchronous, You can use it as it is.
OR
You can make use of NSURLSession for more control
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
self.downloadTask = [self.session downloadTaskWithRequest:request];
[self.downloadTask resume];
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
}

How to get server response data in NSURLSession without completion block

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.
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.

Resources