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.
Related
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.
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?
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
Questions asking for code must demonstrate a minimal understanding of the problem being solved. Include attempted solutions, why they didn't work, and the expected results. See also: Stack Overflow question checklist
Closed 9 years ago.
Improve this question
I have 100 URLs, I want to download all these images and save in Documents. For saving I have done, also I can do lazy loading, but I am unable to download all with minimum time and GUI should not hang.
What is suitable method to do so?
Thanks
Use SDWebImage. You can download it from below url
https://github.com/rs/SDWebImage
For load 100 images using Asynchronous request
for(int i=0; i<99; i++)
{
strImage=[[res valueForKey:#"result"] objectAtIndex:i];
if ([[strImage lowercaseString] hasSuffix:#".jpg"] || [[strImage lowercaseString] hasSuffix:#".png"])
{
//[HUD show:YES];
NSURL *url=[[NSURL alloc]initWithString:strImage];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
thumbnail.image=[UIImage imageWithData:data];
}];
}
}
A sophisticated solution may become quite complex. Downloading and saving 100 URLs is not as easy as 100 times downloading one image.
The most vexing issue that will arise in ambitious but less smart implementations are memory pressures. A third party network library will not automagically solve this problem for you.
One simple - yet viable - approach which tries to avoid memory problems at the cost of a bit performance, is executing the download and save to disk operation all sequentially. This ensures, that only one image will be handled at any time. Thus, you can make safe assumptions about the maximum memory required for this approach.
A solution may look as below:
Suppose, you have an asynchronous method which loads the image data from a given URL:
typedef void (^load_completion_t)(NSData* data, NSError* error);
- (void) loadURL:(NSURL*)url completion:(load_completion_t)completionHandler;
This method will load the whole image data into memory. This isn't really the best way, but IFF we can assume that one image always fits into memory, it becomes a simple solution.
Furthermore, suppose, there is a synchronous method which saves the data to disk:
- (void) saveData:(NSData*)data;
Now, given an array of URLs you can sequentially load and save a number of images as follows:
typedef void(^completion_t)(id result, NSError* error);
- (void) saveImagesWithURLs:(NSMutableArray*)urls
completion:(completion_t)completionHandler
{
if ([urls count] > 0) {
NSURL* url = [urls firstObject];
[urls removeObjectAtIndex:0];
[self loadURL:url completion:^(NSData* imageData, NSError*error){
if (imageData) {
[self saveData:imageData];
[self saveImagesWithURLs:urls completion:completionHandler];
}
else {
// handle error
}
}];
}
else {
// finished
if (completionHandler) {
completionHandler(#"Images saved", nil);
}
}
}
The method above is an "asynchronous loop":
The completion handler of loadURL:completion will call saveImagesWithURLs:completion:, much like a recursive invocation. However, this is NOT a recursive method: when the completion handler of method saveImagesWithURLs:completion:gets executed, saveImagesWithURLs:completion: already returned.
Given a propert which is an array of URLs:
#property (nonatomic, strong) NSArray* imageURLs;
you can invoke the asynchronous loop as shown below:
[self saveImagesWithURLs:[self.imageURLs mutableCopy]
completion:^(id result, NSError*error){
if (error) {
// handle error
}
else {
// result equals #"Images saved"
}
}];
You can call this method from the main thread. It will NOT block, because it is asynchronous. We also assume, that the completion handlers will be invoked on a private queue, and not on the main thread.
Better user AFNetworking, it will help you to download all the images asynchronously (no GUI hang)
https://github.com/AFNetworking/AFNetworking
You can use AFHTTPClient to enqueueBatchOperations and this has a completionBlock which is called when all operations are finished. Should be exactly what you're looking for.
I'm using UIImageView+AFNetworking:setImageWithURLRequest to load images into table view cells. It's working great and I tried using AFNetworkActivityIndicatorManager as in the example below to display the activity indicator while the images are loading.
[[AFNetworkActivityIndicatorManager sharedManager] incrementActivityCount];
[imageView setImageWithURLRequest:request placeholderImage:[UIImage imageNamed:placeholder]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
[[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
}
];
The problem I've run into is when scrolling quickly through the table view AFNetworking cancels certain requests and the AFNetworkActivityIndicatorManager never gets decremented. Canceling requests does keep things fast and appears to be by design:
"As table view cells are recycled, for instance, setting a new image URL will automatically cancel the previous request." https://github.com/AFNetworking/AFNetworking/wiki/Introduction-to-AFNetworking
Is there a way to detect the cancellation so I can decrement the AFNetworkActivityIndicatorManager or is there a better way to make sure the requests and activity indicator stay in sync?
Thanks!
You shouldn't need to do this all you need to do is call:
AFNetworkActivityIndicatorManager.sharedManager.enabled = YES;
in you AppDelegate application:didFinishLaunchingWithOptions: and then AFNetworking looks after hiding and showing the indicator as and when network connections start and stop.
I have some difficulties to set up the correct configuration relative to sendAsynchronousRequest:queue:completionHandler: method (NSURLConnection class).
My scenario is the following:
I set up a singleton class that manages different NSURLConnections. This singleton istance has a NSOperation Queue (called downloadQueue) that makes a request to a web server and retrieves a string path (1).
Once done, the path is used to download a file within a web server (2). Finally, when the file has been correctly downloaded, I need to update the UI (3).
I figured out only the first request: the one through which I'm able to download the path. Could you suggest me a way to perform the other two steps?
Few questions here:
the download queue (downloadQueue) is not the main one, is it possible to open a new NSURLConnection in that queue? In other words, is it correct? (See comments in code snippets)
if the previous question is correct, how can I grab the main queue and update the UI?
Here the code snippet I use to perform the first step where downloadQueue is an instance variable that can be obtain through accessor mehods (#property/#synthesized);
// initializing the queue...
downloadQueue = [[NSOperation alloc] init];
// other code here...
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[self downloadQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if([data length] > 0 && error == nil) {
// here the path (1)
// how to perform a second connection?
// what type of queue do I have to use?
}
}];
You're on the right track for performing your first download.
In the completion handler block after the first download, you're computing the URL that you'll need for a second download, right? Then you can perform that second download the same way: call +[NSURLConnection sendAsynchronousRequest:...] again with the new URL and the same queue. You can do this within the completion block for the first download.
To update the UI after the second download is done, switch to the main queue within the completion block for that download. You can do this with dispatch_async() or dispatch_sync() (in this case it doesn't matter which because you don't have further work to do on the download queue) and dispatch_get_main_queue(), or with -[NSOperationQueue addOperationWithBlock:] and +[NSOperationQueue mainQueue].
Your code should look something like this:
// init download queue
downloadQueue = [[NSOperationQueue alloc] init];
// (1) first download to determine URL for second
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[self downloadQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if([data length] > 0 && error == nil) {
// set newURLRequest to something you get from the data, then...
// (2) second download
[NSURLConnection sendAsynchronousRequest:newURLRequest queue:[self downloadQueue] completionHandler:^(NSURLResponse *newResponse, NSData *newData, NSError *newError) {
if([newData length] > 0 && newError == nil) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// (3) update UI
}];
}
}];
}
}];
For updating the ui, as far as I know, you have to do that on the main thread. The ui could be updated from other threads but those updates are not fully reliable. In an app that I put together that made request to a web service, I make use of dispatch_async() to get access to the main queue and then I update, in my case a table view, from that call.
dispatch_async(dispatch_get_main_queue(), ^{
//block to be run on the main thread
[self.tableView reloadData];
});
I hope this helps.