NSURLSessionDownloadTask Delegates not calling didWriteData method - ios

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.

Related

NSURLSession delegate methods are not called

I'm trying to update an old application to move from NSURLConnection to NSURLSession. Everything works fine when I use blocks for NSURLSession:
NSURLSessionDataTask *dataTask = [session
dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {}
But sometimes I need to analyse the data that the app is receiving from server, so I need didReceiveResponse and didReceiveData methods, similar to the ones used with NSURLConnection. I'm trying to do it like this:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:#"https://www.apple.com"]];
[dataTask resume];
But my delegates are never called:
- (void)URLSession:(NSURLSession *)session
NSURLSessionTaskDelegatedidReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
NSLog(#"-----------------------> NSURLSessionTaskDelegatedidReceiveResponse");
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
NSLog(#"----------------> didReceiveData");
}
Same thing happens when I'm trying to use other types of tasks (NSURLSessionDownloadTask for example), their delegates are never called. I can actually see in the debugger that the tasks are switching to running state, but I can never get any calls to delegates.
In my header file I do conform my ViewController to delegates:
#interface ViewController : UIViewController <NSURLSessionDelegate, NSURLSessionDataDelegate, NSURLSessionTaskDelegate>
I did quite a bit of searching but even when I'm trying some working examples from the web they just stop working as soon as I try to paste them in my project. For example this tutorial works perfectly for me (I do have to add Transport Security permissions to plist file, but that's about it). But when I'm trying to use this code in my project delegates are never called. And when I'm actually paste my code to another simple application it works just fine. So something probably wrong with my project's settings? Could you please show me what can cause something like this? I'm completely bewildered at the moment.
You are using completion blocks. That exclude delegate calls.
https://developer.apple.com/reference/foundation/nsurlsessiondatadelegate?language=objc
Completion handler block are primarily intended as an alternative to
using a custom delegate. If you create a task using a method that
takes a completion handler block, the delegate methods for response
and data delivery are not called.
And the method you want to use has a different name to
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler

Initiating Background transfer service in background fetch ios

My objective is to send data/image to the server when the app is in background. From iOS7, we can do this using background fetch. But the background fetch only offers 30 sec time limit. The data which I am sending to the server may take longer time since it has more images. While googling I came across Background Transfer Service which offers unlimited time to upload/download data in the background. Is it possible to initiate the background transfer service in the background fetch code? If so how to handle it.
Whenever you want to start your upload/download (in your case during your 30secs of background fetch) execute the following lines:
NSString *downloadURLString = //Your link here;
NSURL* downloadURL = [NSURL URLWithString:downloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
// Create a background session
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *identifier = #"com.yourcompany.yourapp";
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
});
//Init a NSURLSessionDownloadTask with the just-created request and resume it
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
[task resume];
});
Also, don't forget to implement those delegate methods:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
For a detailed sample, have a look at this sample app

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.

NSURLSession delegate vs. completionHandler

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

Resources