Originially I thought that if a NSURLSessionDownloadTask finishes successfully URLSession:downloadTask:didFinishDownloadingToURL: method will get called, if it fails for some reason - URLSession:task:didCompleteWithError:.
It works as expected on simulator (only one of this method is called for one download task) but on device this is not the case: in case of failure both these methods are called, URLSession:downloadTask:didFinishDownloadingToURL: being the first the one. (an both these methods pass the same task in parameters)
Is there something I am missing?
I found a solution to this problem:
To get the status code in the response header, you must first start a NSURLSessionDataTask.
This will call the following delegate method URLSession:dataTask:didReceiveResponse:completionHandler:.
In this method, you can first check the status code of the NSURLResponse parameters (by casting it to a NSHTTPURLResponse) and finally call the completion handler with either NSURLSessionResponseBecomeDownload to convert your dataTask to a downloadTask (which will behave as you would expect from a NSURLSessionDownloadTask) or NSURLSessionResponseCancel to avoid downloading some data you don't need (for example if the response's status code is 404).
Also, if you need to do something with the converted NSURLSessionDownloadTask (like storing it in an array or a dictionary or replacing the data task with the new object), it can be done in the URLSession:dataTask:didBecomeDownloadTask:
Hope this helps someone!
According to Apple's documentation under NSURLSessionDownloadDelegate It is standard behavior.
/* Sent when a download task that has completed a download. The delegate should
* copy or move the file at the given location to a new location as it will be
* removed when the delegate message returns. URLSession:task:didCompleteWithError:
* will still be called. */
If you were doing a HTTP request, you can cast the task's response into a NSHTTPURLResponse to get the HTTP status code:
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL )location
{
NSLog(#"response is %d\n" ((NSHTTPURLResponse)downloadTask.response).statusCode);
NSLog(#"error is %#\n", [downloadTask.error localizedDescription]);
}
Apple's logic is that when you get a 404, error will still be null.
Use completion block instead of delegate:
NSURLSessionDownloadTask *mySessionDownloadTask = [myURLSession downloadTaskWithRequest:myRequest completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error)
{
dispatch_async(dispatch_get_main_queue(), ^{
if(!error)
{
// Finish loading
}
else
{
// Handle error
});
}];
Note: If you don't get the main queue, any update related to user interface will be retarded which causes unsuspected behaviors.
NSURLSessionDownloadTask is a subclass of NSURLSessionTask, which has an error property. Could you check that in your URLSession:downloadTask:didFinishDownloadingToURL: delegate method before you attempt to copy your file?
Related
I am experiencing a strange issue from time to with my NSURLSessionDownloadTasks (using a background download configuration).
It is always related to errors with NSURLDomain code NSURLErrorBackgroundSessionWasDisconnected (-997). The error itself usually seems to be coming from the app generically crashing or being forcefully closed during development from Xcode.
The relevant code boils down to
- (void)download:(NSURLRequest *)request
{
NSURLSessionDownloadTask *downloadTask = [self.urlSession downloadTaskWithRequest:request];
[downloadTask resume];
}
- (void) URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)tempLocation
{
NSLog(#"task %# didFinishDownloadingToURL with status code %#", downloadTask, #([downloadTask.response statusCode]));
}
- (void) URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
NSLog(#"task %# didCompleteWithError %#", task, error);
}
The problem in these cases is, that the same NSURLSessionDownloadTask gets two (instead of only one expected) call in -[NSURLSessionTaskDelegate URLSession:task:didCompleteWithError:]. Once without an error (which aligns to a call with a 200 OK response code to -[NSURLSessionDownloadDelegate URLSession:downloadTask:didFinishDownloadingToURL:]) and then another time, usually a couple seconds later with an error (-997). Both times it's exactly the same memory address for the task being passed into these delegate methods.
Has anybody else experienced something similar? I am not expecting that second callback after I was already told that my task succeeded. Is there any obvious reason why the NSURLSession may still hold on to a task it reportedly finished already but then all of a sudden thinks it should inform me that it's lost connectivity to it background transfer service?
FWIW, I had a similar issue because I was updating the UI from the NSURLSession delegate methods, which run in background thread.
So, the solution for me was to move the UI-related code to run on the main thread as below:
dispatch_async(dispatch_get_main_queue(), ^{
//code updating the UI.
});
In my App I get JSON from a Web service. This JSON contains the urls of several files that I would like to download.
I want to download each file one by one (wait for the first download to finished until the second one starts and so an and so forth) using NSURLSessionDownloadTask. I would also like to keep track of the total bytes written so I can update UI.
Thanks a lot in advance !
NSURLSessionDownloadTask's as you well know do not play very nicely with NSOperationQueues unlike their counterpart the NSURLConnection (where it could be encapsulated inside an NSOperation).
One option would be to add all your urls to an array, and then inside the completionHandler of the task, simply queue the next item.
So you might create your tasks in a loop, call a progressBlock inside each tasks completion handler, store the tasks in an array, and queue the next task inside each tasks completion handler:
- (void)addRequestsWithURLs:(NSArray *)urls
progressBlock:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations, NSURLSessionDownloadTask *task,NSURL *location, NSURLResponse *response, NSError *error))progressBlock {
__block NSUInteger numberOfFinishedOperations = 0;
NSUInteger totalNumberOfOperations = [urls count];
for (NSString *url in urls) {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
__block NSURLSessionDownloadTask *task = [self.session downloadTaskWithRequest:request
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
//downloadFileSomewhere
++numberOfFinishedOperations;
if (progressBlock) {
progressBlock(numberOfFinishedOperations, totalNumberOfOperations,task,destination != nil ? [NSURL fileURLWithPath:destination] : nil,response,error);
}
//queueNext
[self processCompletedTask:task];
}];
//stores an array of NSURLSessionTasks
[self.tasksWaitingToBeQueued addObject:task];
}
}
- (void)processCompletedTask:(NSURLSessionTask *)completedTask {
//clean up and queue next one
[self.tasksWaitingToBeQueued removeObject:completedTask];
nextTask = [self.tasksWaitingToBeQueued firstObject];
if (nextTask) {
[nextTask resume];
}
}
NOTE
In this example I show progress as the number of tasks completed and not the number of bytes, this is the recommended approach (its also simpler). To indicate progress using bytes you would need to know the total number of bytes to download beforehand (since you want to show a progress bar) and also implement the NSURLSession delegate and monitor the progress of each task, capture the bytes downloaded and update your block. If your server doesn't tell you the total number of bytes then you would probably need to do a HEAD request for every resource and aggregate the sizes. Personally this solution is way to complicated for what could simply be resolved by indicating progress as the number of files downloaded.
To achieve this might look something like this:
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
self.totalBytesWritten += totalBytesWritten;
NSUInteger totalProgressSoFar = self.totalBytesWritten;
NSUInteger totalExpectedBytes = self.totalExpectedBytes;
//you would need to capture some progress block locally - beware of retain cycles
self.progressBlock(totalProgressSoFar/totalExpectedBytes)
}
when you finish you should set the progressBlock to nil to prevent any retain cycles.
My app fetches some items from web server like below:
for (photo in photoList) {
NSArray *comment = [self fetchCommentsFromServer:photo.photoId];
[photo setComment:comment];
}
fetchCommentFromServer makes asynchronous http call with dispatch_async.
dispatch_queue_t queue = dispatch_queue_create("autantication_queue", 0);
dispatch_async( queue, ^{
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (success) {
success(responseObject);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (failure) {
failure(error);
}
}
];
});
It gives error because of comment isn't ready when I try to attach it to photo.
How can I guarantee that the comment is ready when it's attached to photo?
I tried to use semaphores but it made the call too slow.
fetchCommentsFromServer needs a completion block. This can be executed whenever the network call has finished. Alternatively, you can work on each photo after the fetch is complete, as part of the network call completion block.
The only way you can guarantee the network fetch is finished when you want to work on the results is to not try and do any work until the network fetch is finished. This is pretty much the point of all these completion blocks and delegate methods.
The method you call to get the data executes then the next line sets the message without the response coming back. You should modify fetchCommentsFromServer to have a completion block where you then set the comment inside the block (ensuring the request has completed before trying to modify it).
On a side note, make sure to jump back on the main thread to modify any UI elements (AKA your label).
fetchCommentsFromServer: can't return anything because the data that it wants to return isn't available until after the method has completed. Instead, the method should take a completion block as a parameters and pass the data back using that. In your case, this will be called from the AFNetworking success block.
I think I am asking a silly question but just curious.
Is there any way to know if all the images are downloaded.
My motive behind this is that I want to invoke one function once all the images are downloaded.
I am using UIImageview setImageWithURL in a for loop
Thanks in Advance :)
You'll want to use UIImageView+AFNetworking's setImageWithURLRequest method. The success block is executed when the image has finished loading.
#param urlRequest The URL request used for the image request.
#param placeholderImage The image to be set initially, until the image request finishes. If `nil`, the image view will not change its image until the image request finishes.
#param success A block to be executed when the image request operation finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the image created from the response data of request. If the image was returned from cache, the request and response parameters will be `nil`.
#param failure A block object to be executed when the image request operation finishes unsuccessfully, or that finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the error object describing the network or parsing error that occurred.
*/
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(UIImage *)placeholderImage
success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;
Since setImageWithURL is asynchronous, it makes it a bit difficult to keep track of whether or not all the images have been downloaded. One solution that might work would be to use the setImageWithURLRequest:placeholderImage:success:failure: method, which allows you to execute code whenever the image URL request succeeds or fails.
Since you're running a for loop, you probably have a fixed number of images to run through. In this case, you can set up a property that keeps track of the images that have been downloaded in the success/failure blocks. When this value is equal to the number that you're wanting, then you can run some kind of logic (i.e. post a notification, delegation) to trigger that all downloads have been completed. (Or, if there were any failures, you can add some logic for retrying/post a message saying there was an error, etc.)
For example (assume numberOfImagesToDownload is some constant value set):
- (void)processImageForURL:(NSURL *)url {
// Assume `placeholderImage` is a reference to an image.
[imageView setImageWithURLRequest:[NSURLRequest requestWithURL:url]
placeholderImage:placeholderImage
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
// Logic here to set the imageView's image (for the example's sake, assume
// we have access to the respective UIImageView.)
imageView.image = image;
// Hold onto a property to keep track of how many images you've downloaded,
// under the assumption that there's a set number of images you need to download.
// Since you're running this under a for loop, you could probably check if the
// for's max condition is equal to the number of downloaded images.
self.numberOfImagesDownloaded++;
if(self.numberOfImagesDownloaded == numberOfImagesToDownload) {
// All images have been downloaded.
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
// You can also keep track of which images failed, if that's important.
}];
}
You can queue your requests using NSOperationQueue and use KVO to observe the operations property of your queue, then you can tell if your queue has completed by checking for [queue.operations count] == 0.
Add a observer of the operations property:
[self.queue addObserver:self forKeyPath:#"operations" options:0 context:NULL];
Handle the event when operations count hits 0:
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (object == self.queue && [keyPath isEqualToString:#"operations"])
{
if ([self.queue.operations count] == 0)
{
// Your downloads have completed
}
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
And you add your requests like this:
AFImageRequestOperation *imgRequest = [AFImageRequestOperation imageRequestOperationWithRequest:urlRequest success:^(UIImage *image) { }
[self.queue addOperation: imgRequest]
This is only a pseudocode. I haven't tested it but it should point you to the right direction.
I'm currently doing this:
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
and then I update the UI and run alot of animations to display the data that I just received.
However I am now trying to use an asynchronous request to load the info instead as the above method is locking the main thread.
Any ideas? i've tried setting up an NSOperationQueue and using:
NSData *responseGBP = [NSURLConnection sendAsynchronousRequest:requestGBP queue:operationQueue completionHandler:nil];
However I get this error: Initializing 'NSData *__strong' with an expression of incompatible type 'void'
Can you geniuses help me?
sendAsynchronousRequest:queue:completionHandler: returns void, so you can't initialize the NSData object immediately, you need to wait the response, which is asynchronous. So just do something like this:
[NSURLConnection sendAsynchronousRequest:requestGBP queue:operationQueue completionHandler: ^(NSURLResponse* response, NSData* data, NSError* error)
{
responseBGP= data;
// Additional code handling the result goes here, not after the call.
}];
// Here responseBGP may be nil as well, you don't know when the concurrent
// operation will finish.
Notice that after you have called this method, isn't said that responseBGP will be initialized, because the method is asynchronous, executed on the queue.