I want to display an image on the screen which I take from the internet. I have used
NSURLConnection to create an asynchronous call to take the data and, in the response block, I called the code to assign it to an UIImage object.
My question is why do I need to call sleep(1) after the block execution? If i'm not calling it, then my image is not drawn on the screen. Is it another, more elegant way to achive this?
-(void)loadImage:(NSString *)url
{
NSURL *imageURL = [NSURL URLWithString:url];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageURL cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0f];
[NSURLConnection sendAsynchronousRequest:imageRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if(!connectionError) {
if(data) {
//there goes the main thingy
self.myView.wallpaperImage = [UIImage imageWithData:data];
[self.myView setNeedsDisplay];
} else {
NSLog(#"No data found at url:%#",url);
}
} else {
NSLog(#"Could not connect to %#",url);
}
}];
sleep(1);
}
This:
self.myView.wallpaperImage = [UIImage imageWithData:data];
[self.myView setNeedsDisplay];
Is happening on the thread managed by the NSOperationQueue passed to sendAsynchronousRequest. Those methods need to be called from the main thread.
Your sleep may be causing the main thread's runloop to iterate, after which those calls appear to have worked.
To fix this, and to avoid a whole bunch of other problems your current approach will have, do this:
[NSURLConnection sendAsynchronousRequest:imageRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([data length] > 0) {
//there goes the main thingy
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.myView.wallpaperImage = [UIImage imageWithData:data];
[self.myView setNeedsDisplay];
}];
} else {
// Perform your error handling here.
}
}];
This will use [NSOperationQueue mainQueue] to perform those UIKit calls from the main queue - not libdispatch. libdispatch is a low level interface, it is a recommended best practice to always prefer the higher level interface - in this case, NSOperationQueue. UIKit is only safe when called from the main thread (or queue).
It also changes your error handling behavior to follow the best practices for the platform - check the result of your call (in this case, data) and THEN process any error returned.
Your code is actually a good example of why blocks retain captured objects (in this case self). If there was no retain cycle here, ARC could destroy queue as soon as it goes out of scope, and the block would never execute. Instead, because of the retain cycle, the queue stays around until the block has executed.
Related
I have a problem. I call some method in dispatch_async. But in callMethod2 in different object I am uploading image with [NSURLConnection sendAsynchronousRequest. But after upload, It doesn't show me response. (However when I call callMethod2 without dispatch_async, it works great). Where can be the problem?
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
[offline callMethod1];
[offline callMethod2];
});
Upload image
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog("Never show me me this log");
}];
You're calling NSLog on a thread that isn't the main thread (aka calling it asynchronously) so you won't see NSLog as it must run on the main thread.
You can have the completion block notify you some other way when it's done. The best way I've found is using https://github.com/kseebaldt/deferred which allows you to send a promise saying (I promise I'll do this thing and notify you when it's done).
The following code is an attempt to me better understand [NSURLConnection sendAsynchronousRequest:queue:completionHandler].
There are NSLog statements in the completionHandler block, but when I run this in main.m in XCode from a command line project, it never enters the completionHandler blocks. I've tried using the different queues, mainQueue and currentQueue but neither work.
My hunch is that the queue is being deallocated before the request is completed and that retain cycles are involved.
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
#autoreleasepool {
NSCache *myCache = [[NSCache alloc] init];
NSArray *images = #[
#"http://i.stack.imgur.com/E66qr.png",
#"http://www.tiempoyquimera.com/wp-content/uploads/2010/01/Euro-Trash-Girl-2010.jpg",
#"http://1.bp.blogspot.com/-Mxd8AB2nbQY/UYCISJiQz3I/AAAAAAAAAH8/Tc43U8aa9dM/s1600/Tarantino10colhans_1460858i.jpg",
#"https://awestruckwanderer.files.wordpress.com/2014/02/alan-watts.png",
#"http://www.esalen.org/sites/default/files/photo_images/20120201_DELLIS__MG_9612_711.jpg"];
for (NSString *image in images){
NSURL *myURL = [NSURL URLWithString:image];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:myURL];
NSLog(#"Can handle request %#", #([NSURLConnection canHandleRequest:request]));
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(#"In the completion handler");
if (!error)
{
// save data to cache with url as key
NSLog(#"Image Added to Cache");
[myCache setObject:data
forKey:myURL];
} else
{
NSLog(#"Image Not Added to Cache");
}
}];
}
}
return 0;
}
My hunch is that the queue is being deallocated before the request is completed and that retain cycles are involved
Not quite. Retain cycles are not involved. Persistence is involved. You are doing this in a main function. It exits immediately - the asynchronous stuff (the networking and the subsequent callback) is asynchronous, so it would come later, if we had any persistence. But we don't. main exits, and that means that the whole darned program is torn down, kaboom, before there is any opportunity to do any networking, let alone call back into the completion handler after the networking.
Now contrast this with how things happen in real life. In a real iOS app, main does not exit, because it calls UIApplicationMain, which loops until the app is terminated.
int main(int argc, char *argv[])
{
#autoreleasepool {
return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
}
}
In that code, UIApplicationMain just keeps running until it is aborted or otherwise terminated. Meanwhile, classes and instance have sprung to life, and they persist, because UIApplicationMain does not stop. For example:
#implementation MyViewController
- (void) someMethod {
// ...
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// ...
}
}
#end
Now, in one sense, exactly the same thing happens: someMethod exits immediately. But our program overall is still running! UIApplicationMain has a run loop and that run loop is still cycling. Thus, things live on, and so now the asynchronous material can happen - we can network and then call the callback.
I have a problem with my application.It freeze for several second when I tap the sidebar menu.
What happen when I tapped menu is I pass string that gonna be url for json data fetch in my mainviewcontroller.Then it freeze because I fetch the data and populating data in tableview.
However I really new to ios programming,I wonder how can I remove the freeze?.
thanks in advance
here is my code snippet for the mainviewcontroller:
Don't use dataWiyhContentsOfURL:, or at least not directly on the main thread. If you block the main thread then the whole app stops working (as you see).
You need to learn about background threads and callback blocks, and look at using NSURLSession to download your data and then process it.
Instead of using dataWithContentsOfURL (which will block the main thread and so the UI) you need to start an asynchronous connection. In the IF ELSE change the two requests to something like below. The completionHandler (Block) is executed when done, the data parsed, HUD removed and table Updated.
You can even (and in fact must) do this within your cellForRowAtIndexPath for each of the images, however, I would use SDWebImage as it has a cache and is very easy to use.
There are also other methods if this is not right for you such as NSURLSession.
Some other points;
I have also noted that the HUD is stopped on every iteration of the FOR and probably should be outside.
I also can not see how your data is being loaded so I added a [myTable reloadData];
I can not see that the "dictionary" object is needed as it can be added directly to the array (see code)
// If you have the status bar showing
// [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[HUD showUIBlockingIndicatorWithText:#"Please wait. . ."];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kategori]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
// [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if (data != nil && error == nil)
{
//All Worked
id jsonObjects = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
for (NSDictionary *dataDict in jsonObjects)
{
NSString *title_data = [dataDict objectForKey:#"title"];
NSString *thumbnail_data = [dataDict objectForKey:#"thumb"];
NSString *author_data = [dataDict objectForKey:#"creator"];
NSString *link_data = [dataDict objectForKey:#"link"];
[myObject addObject:[[NSDictionary alloc]initWithObjectsAndKeys:
title_data, title,
thumbnail_data, thumbnail,
author_data,author,
link_data,link,
nil]];
}
[HUD hideUIBlockingIndicator];
[myTableView reloadData];
}
else
{
// There was an error
}
}];
For the images something like (this is not tested). I am not sure what format your images are in but you should be able to just add it, this may need tweeking;
cell.imageView.frame = CGRectMake(0, 0, 80, 70);
__block UIImageView *cellImage = cell.imageView;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[tmpDict objectForKey:thumbnail]]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (data != nil && error == nil)
{
//All Worked
cellImage.image = [[UIImage alloc]initWithData:data];
[cellImage layoutIfNeeded];
}
else
{
// There was an error
}
}];
You can start activity indicator and call fetch data method after few time...
- (void)viewDidLoad{
[activityIndicator startAnimating];
[self performSelector:#selector(fetchData) withObject:nil afterDelay:0.5];
}
- (void)fetchData{
Fetch your data over here
}
Or ideally you have to load data Asynchronous
For loading data Asynchronously check out the following link-
iphone-synchronous-and-asynchronous-json-parse
I Prefer MBProgressHUD.
Here is the link for 3rd Party API.
https://github.com/jdg/MBProgressHUD
Just copy these two files in your app.
MBProgressHUD.h
MBProgressHUD.m
I am doing a lot of URL requests (about 60 small images) and I have started to do them Asynchronously. My code adds another image (little downloading thing) and then sets a Request going.
When the request is done I want "data" to be put in the location which was originally added for it, however, I can not see how to pass "imageLocation" to the block for it to store the image in the correct location.
I have replaced the 3rd line with below which seems to work but I am not 100% it is correct (it is very hard to tell as the images are nearly identical). I am also thinking that it is possible to pass "imageLocation" at the point where the block is declared.
Can any confirm any of this?
__block int imageLocation = [allImages count] - 1;
// Add another image to MArray
[allImages addObject:[UIImage imageNamed:#"downloading.png"]];
imageLocation = [allImages count] - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil && error == nil)
{
//All Worked
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageWithData:data]];
}
else
{
// There was an error, alert the user
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageNamed:#"error.png"]];
}];
Dealing with asynchronous methods is a pain ;)
In your case its guaranteed that the completion block will execute on the specified queue. However, you need to ensure that the queue has a max concurrent operations count of 1, otherwise concurrent access to shared resources is not safe. That's a classic race http://en.wikipedia.org/wiki/Race_condition. The max concurrent operations of a NSOperationQueue can be set with a property.
In general, completion handlers may execute on any thread, unless otherwise specified.
Dealing with asynchronous methods gets a lot easier when using a concept called "Promises" http://en.wikipedia.org/wiki/Promise_(programming). Basically, "Promises" represent a result that will be evaluated in the future - nonetheless the promise itself is immediately available. Similar concepts are named "futures" or "deferred".
There is an implementation of a promise in Objective-C on GitHub: RXPromise. When using it you also get safe access from within the handler blocks to shared resources. An implementation would look as follows:
-(RXPromise*) fetchImageFromURL:(NSString*)urlString queue:(NSOperationQueue*) queue
{
#autoreleasepool {
RXPromise* promise = [[RXPromise alloc] init];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil) {
[promise fulfillWithValue:data];
}
else { // There was an error
[promise rejectWithReason:error];
};
}];
return promise;
}
}
Then call it:
- (void) fetchImages {
...
for (NSUInteger index = 0; index < N; ++index)
{
NSString* urlString = ...
[self fetchImageFromURL:urlString, self.queue]
.then(^id(id data){
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageWithData:data]];
return #"OK";
},
^id(NSError* error) {
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageNamed:#"error.png"]];
return error;
});
}
}
A couple of thoughts:
If you want to download 60 images, I would not advise using a serial queue for the download (e.g. do not use an operation queue with maxConcurrentOperationCount of 1), but rather use a concurrent queue. You will want to synchronize the updates to make sure your code is thread-safe (and this is easily done by dispatching the updates to a serial queue, such as the main queue, for that final update of the model), but I wouldn't suggest using a serial queue for the download itself, as that will be much slower.
If you want to use the NSURLConnection convenience methods, I'd suggest something like the following concurrent operation request approach (where, because it's on a background queue, I'm using sendSynchronousRequest rather than sendAsynchronousRequest), where I'll assume you have an NSArray, imageURLs, of NSURL objects for the URLs of your images:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:&response error:&error];
if (!data) {
NSLog(#"%s sendSynchronousRequest error: %#", __FUNCTION__, error);
} else {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self.allImages replaceObjectAtIndex:idx withObject:image];
});
}
}
}];
[queue addOperation:operation];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
A couple of asides: First, I'm using an operation queue rather than a GCD concurrent queue, because it's important to be able to constrain the degree on concurrency. Second, I've added a completion operation, because I assume it would be useful to know when all the downloads are done, but if you don't need that, the code is obviously simper. Third, that benchmarking code using CFAbsoluteTime is unnecessary, but useful solely for diagnostic purposes if you want to compare the performance using a maxConcurrentOperationCount of 4 versus 1.
Better than using the NSURLConnection convenience methods, above, you might want to use a NSOperation-based network request. You can write your own, or better, use a proven solution, like AFNetworking. That might look like:
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFImageResponseSerializer serializer];
manager.operationQueue.maxConcurrentOperationCount = 4;
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [manager GET:[url absoluteString] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self.allImages replaceObjectAtIndex:idx withObject:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%s image request error: %#", __FUNCTION__, error);
}];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
Because AFNetworking dispatches those completion blocks back to the main queue, that solves the synchronization issues, while still enjoying the concurrent network requests.
But the main take-home message here is that an NSOperation-based network request (or at least one that uses NSURLConnectionDataDelegatemethods) opens additional opportunities (e.g. you can cancel all of those network requests if you have to, you can get progress updates, etc.).
Frankly, having walked through two solutions that illustrate how to download the images efficiently up-front, I feel compelled to point out that this is an inherently inefficient process. I might suggest a "lazy" image loading process, that requests the images asynchronously as they're needed, in a just-in-time (a.k.a. "lazy") manner. The easiest solution for this is to use a UIImageView category, such as provided by AFNetworking or SDWebImage. (I'd use AFNetworking's if you're using AFNetworking already for other purposes, but I think that SDWebImage's UIImageView category is a little stronger.) These not only seamlessly load the images asynchronously, but offer a host of other advantages such as cacheing, more efficient memory usage, etc. And, it's as simple as:
[imageView setImageWithURL:url placeholder:[UIImage imageNamed:#"placeholder"]];
Just a few thoughts on efficiently performing network requests. I hope that helps.
I currently have the following code in an NSOperation that has an observer for keyPath "isCancelled":
downloaded = FALSE;
NSURL *url = [NSURL URLWithString:requestString];
dataXML = [[NSData alloc] initWithContentsOfURL:url];
downloaded = TRUE;
I want to make it so that the observeValueForKeyPath function is able to cancel the dataXML continuing or just completely stop the NSOperation once the NSOperation is sent a cancel message. The NSOperation's cancelling operation cancel only notifies the operation that it should stop, but will not force my operation's code to stop.
You can't cancel it.
If you want to be able to cancel the load mid-way through, use NSURLConnection operating in asynchronous mode. It's a bit more work to set up but you can cancel at any point in the download process.
Alternatively, you could use this handy class I wrote that wraps an async NSURLConnection and its delegate in a single method call ;-)
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[RequestQueue mainQueue] addRequest:request completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data && error == nil)
{
//do something with your downloaded data
}
}];
//to cancel the download at any time, just say
[[RequestQueue mainQueue] cancelRequest:request];
Easy!
</shamelessSelfPromotion>
Note that the request above is already asynchronous, and the class already manages queuing of multiple requests, so you don't need to (and shouldn't) wrap it in an NSOperationQueue.