On iOS, how to make a cell.imageView refresh its content? - iphone

I am experimenting in tableView:cellForRowAtIndexPath to set an image by calling
[self downloadImage:urlString andSetIntoCellImageView:cell]
and in downloadImage, it will call NSURLConnection:sendAsynchronousRequest (iOS 5 and up only), and in the completion block, set the image by using
cell.imageView.image = [UIImage imageWithData:data]; // data is downloaded data
and it works if in tableView:cellForRowAtIndexPath, the imageView is populated with a dummy placeholder image -- and I wonder how the new image is refreshed, is it by setNeedsDisplay to do a repaint? But if I don't set the placeholder image, then the new image won't show at all. I wonder what mechanism can be used to make it show the image?
If I use
[cell.imageView setNeedsDisplay]
or
[cell setNeedsDisplay];
in the completion block, it won't work, and if I use
[self.table reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
in the completion block by making downloadImage accept the indexPath, it will call tableView:cellForRowAtIndexPath again, and cause an infinite loop. So it seems like I need to use some hash table to remember if the image is already in hash table: if not, call downloadImage, and if in hash table, simply use it, so there will be no infinite loop.
But is there an easy way to cause the image to show up? Setting a placeholder image works but what if we don't -- by what mechanism does placeholder cause the refresh of image?

When a UITableViewCell's -layoutSubviews method is called, if its imageView's image property is nil, imageView is given a frame of (0,0,0,0). Also, -layoutSubviews only is to be called in some situations: when the cell is about to become visible and when it is selected. Not during normal scrolling. So what you've seen is that setting the placeholder inside tableView:cellForRowAtIndexPath: sizes cell.imageView to a non-zero size and subsequent changes of the image will be visible.
I fixed the issue by calling [cell setNeedsLayout] in the completion handler, like so:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:MY_IMAGE_URL]];
[NSURLConnection sendAsynchronousRequest:request
queue:self.operationQueue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIImage *image = [UIImage imageWithData:data];
cell.imageView.image = image;
[cell setNeedsLayout];
}];
I found the completion block happens in the background so that necessitates performing my UI work on the main thread. Of course this solution won't account for cell reuse and so forth, but at least solves why the cell's image wouldn't appear :)
Hope this helps!

This doesn't answer your question directly, but a simple workround to your problem would be to use SDWebImage in your cellForRowAtIndexPath: instead. The example on their README page does exactly what you are trying to do.

Related

iPhone - GCD doesn't work for the first time

In my app, I load image from the URL:
-(void)loadImages
{
...
image1 = [UIImage imageWithData:[NSData dataWithContentsOfURL:imgUrl1]];
}
In order to avoid blocking the main thread until the download has completed I call this method in
-viewDidAppear using GCD:
dispatch_async( dispatch_get_global_queue(0,0), ^{
[self loadImages];
});
However, when I open my view controller with the imageView at the first time, the imageView is empty (even if I wait for a long time) but after I open this view controller again and image appears and everything is good to go.
Where is my mistake ?
Sorry, new to multithreading :)
EDIT:
I also forgot to mention, that I use the image in the tableView when I get it:
cell.imageView.image = image1;
This may not be the answer that you were looking for, but that's not the recommended way to load URLs. You should use the URL loading classes available such as NSURLRequest and NSURLConnection.
Try this:
NSURLRequest *imageRequest = [[NSURLRequest alloc] initWithURL:imageURL];
[NSURLConnection sendAsynchronousRequest:imageRequest queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
UIImage *image = [[UIImage alloc] initWithData:data];
[imageView setImage:image];
}
}];
UIElements in iOS should always be updated via main thread.
What you could do is:-
__block NSData *data;
dispatch_queue_t myQueue = dispatch_queue_create("com.appName", NULL);
dispatch_async(myQueue, ^{
data = [NSData dataWithContentsOfURL:imgUrl1];
dispatch_async(dispatch_get_main_queue(), ^(void) {
cell.imageView.image = [UIImage imageWithData = data];
});
});
or Else you there is a better way of fetching images from URL by using AFNetworking. It is faster and easier.
You just have to write one line of code:-
[cell.imageView setImageWithURL:imgUrl1];
You have many problems here:
You can not start networking requests on an thread not running a runloop.
You can not update your UI from a thread other than the main thread.
[NSData dataWithContentsOfURL:imgUrl1] is not a safe way to load an external resource, even on a different thread (but especially on the main thread).
Any time you dispatch to a different thread, you run the risk that your table cell has been recycled and is no longer showing the data you think it is. (It's still the same cell instance, but is now showing some other row's data.)
What you should be doing:
Start your network operation using asynchronous calls on the main thread. (You can use another thread or queue if you want, but you need to make sure it's running a runloop.)
From your delegate messages, dispatch your image decoding on a different thread.
After the image is decoded, dispatch back to the main thread to update it.
Before actually assigning the image, check that the cell is still being used for the purpose you think.
You can solve the first three problems by using AFNetworking. It wraps the delegate methods and lets you just provide a success and failure block. AFNetworking's AFImageRequestOperation in particular bounces code between queues as I've described. (It even runs its main networking loop in a different thread, which isn't necessary but since it does it well, why not?)
You'll still need to verify the cell's identity.
You need to inform the view that the image needs to be redrawn. Add:
[imageView setNeedsDisplay];
to the end of your loadImages method.
Since you are using it in TableView, add [self.tableView reloadData]; in the end of loadImages method.

How to use UIactivityindicatorview while downloading an image from a url

In my app I have implemented a method to download a image from a URL and display it in a TableView cell. The code is working fine but I also need to implement two more functions,
1) a UIActivityIndicator to show that the image is being downloaded.
2) Check whether the image is already downloaded. Download only if the cell is empty.
Here is my code.
-(void) downloadImage
{
NSURL *url = [NSURL URLWithString:#"http://cdn.iphonehacks.com/wp-content/uploads/2012/09/iphone5-front-back.jpg"];
UIImage *image = [[UIImage alloc] initWithData: [NSData dataWithContentsOfURL:url]];
[dCellImageView setImage:image];
[image release];
}
Thanks.
Use [NSData dataWithContentsOfURL:URL options:NSDataReadingMapped error:&error]. It is a synchronized method. So probably start activityIndicator before sending the message, and check error, then stop activityIndicator. What do you mean by 'cell is empty'? Do you store image in disk or just memory. If former, use NSFileManager to check if file exists.
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
It'll show an indicator on the left of the status bar. You can also use MBProgressHUD if you like.
And if you want to check whether the image has downloaded already before downloading, you need to store a key in your Core Data or Property List.

SDWebImage - how to load image from Cache?

I use SDWebImage to download images into my UITableView using:
[cell.imageView setImageWithURL:[NSURL URLWithString:tempPhotoURL] placeholderImage:[UIImage imageNamed:#"temp.jpg"]];
This works perfectly - no problem there. But when I click on any given row in my TableView, I want to load that row's thumbnail-image into a UIImageView that's sitting in the oncoming detail-screen to which I'm navigating. Well how do I do this? Do I now have to get the image from the cache since its already been downloaded? If so, what's the method/process for this? And if that's not the way to go, what is?
I can't figure it out from all those files and methods included in the SDWebImage library...
Simply use almost exactly same code to set the image - SDWebImage will check if it is already cached and if so, it will use it instead of fetching image from the url.
Try this,
[self.imgView sd_setImageWithURL:[NSURL URLWithString:imageUrl placeholderImage:placeholderUrl options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
//Progress block
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL){
//Completion block
}];
There are many ways to achieve things when coding.
One that springs to mind is to ensure the UIImageView is an accessible property of your detail view controller (part of the detail view controller's API).
Then all you need to do is before you push your detail view controller, assign the cell's image view to the detail view's UIImageView property.
OR
create a convenience initialiser and pass the cell's imageview to it...
etc etc.
Call the same method, you downloaded with in your TableView in DetailView when you set the picture, it will check the cache first. Here's the code snippet to download image with SDWebImage
[yourImageView sd_setImageWithURL:[NSURL URLWithString:yourImageUrl]
placeholderImage:[UIImage imageNamed:#"placeholder.jpg"]];

NSOperation mainQueue issue

I have slideshow, and I want to show Big images, I added to [NSOperation mainQueue] operation with low priority, this operation shows the image.
If image is small , everything is OK, but when image is about 5Mb, the view freeze for 1 second, and I can't scroll my slideshow. I think, that displaying big images just so difficult for iPhone, that main queue is too overloaded.
But I don't inderstand it , because all my displaying code is executed in low priority operation.
Here is the displaying code.
[imageView removeFromSuperview];
imageView = nil;
// reset our zoomScale to 1.0 before doing any further calculations
self.zoomScale = 1.0;
// make a new UIImageView for the new image
self.imageView = [[[UIImageView alloc] initWithImage:image] autorelease];
[self addSubview:imageView];
self.contentSize = [image size];
[self setMaxMinZoomScalesForCurrentBounds];
self.zoomScale = self.minimumZoomScale;
May be I can set the priority for gesture recognizers (the regular questure recognizers for UIScrollView?)
Update
Please look at my new topic, I described the issue more properly my topik
Priority has to do with scheduling. If you queue up a bunch of operations during a runloop iteration then they will be executed by their priority on that queue.
One solution to speed this up would be to either include resources that are scaled to the exact size that you are displaying them in. If you are trying to show a 2000x2000 px image in a 200x200 area then the system to scale all this stuff in memory. You can also dynamically create smaller to fit images programmatically. This can be done on a background queue so your UI is still responsive.
How to resize the image programmatically in objective-c in iphone
If I understand you correclty and you have done something like
NSOperationQueue* queue = [NSOperationQueue mainQueue];
Then any NSOperation you add to it will execute on the main dispatch queue which is concurrent (and responsible for executing tasks on the main thread). That would explain the freeze. You can create your own queue which would start a thread for every NSOperation and would free the main thread to render the UI normally as:
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
This however would cause a problem. When the image is finished loading and you pass it to an UIImageView on the screen there will be a big delay until the Image is actually rendered because the main (UI) thread will not be aware of the action until it chooses to refresh (a few seconds later). The solution to this is to add a 'performInMainThread' message to the end of the main method of the NSOperation as such:
-(void)main {
NSData *bgImageData = [NSData dataWithContentsOfURL:self.targetUrl];
UIImage *img = [UIImage imageWithData:bgImageData];
[self performSelectorOnMainThread:#selector(insertImageLoaded:)
withObject:img
waitUntilDone:YES];
}
The 'insertImageLoaded' is in the NSOperation and would load call a setImage:(UIImage*)img to the component you want.

UIImage in uitableViewcell slowdowns scrolling table

I am loading image with data in my table view. Images are coming from web. I created method to get image url in model class.Model class has Nsdictionary and objects.
But this images is slowing down scrolling .Here is my code
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#",
[(Tweet *)[recentTweets objectAtIndex:indexPath.row]urlString]]]]];
cell.imageView.image = image;
Please tell Where I am going wrong?
Use lazy loading and do your own drawing. Try to understand the techniques on the sample projects I linked. These are the best ways to improve the performance of tables with images.
here is the methodology I use for loading images into a UITableView from a remote location:
in your .h file, declare a mutable dictionary for storing the images:
NSMutableDictionary *images;
initialize the dictionary in -init... or in -viewDidLoad
images = [[NSMutableDictionary alloc]init];
in the .m, tableView:cellForRowAtIndexPath:, see if the image exists in your dictionary for the indexPath
UIImage *img = [images objectForKey: indexPath];
if the image does exist, just set it.
if (img) cell.imageView.image = img;
if the image does NOT exist, set the cell's image to a temporary image...
if (!img) cell.imageView.image = [UIImage imageNamed:#"imageUnavailable.png"];
AND add that image to your dictionary so it doesnt try to refetch the image if you scroll off and back to that image before it loads...
[images setObject:[UIImage imageNamed:#"imageUnvailable.png"] forKey: indexPath];
then, in this same block, use an NSOperationQueue, and a custom NSOperation ( here is a reference - NSOperation and SetImage) to get your image and call back into your UITableViewController with that image.
in your callback, add your image to the dictionary (overwriting the temp image) and call [tableView reloadData]
this will give you a nice non blocking user experience.
The are a couple of ways how to do it. I had the best experience with a Queue for the HttpRequests, which I pause during the scrolling process.
I highly recommend this framework for that:
http://allseeing-i.com/ASIHTTPRequest/
I also implemented an image cache, which only loads the images if there weren't in the cache.
And the last tweak was to actually draw the images instead of using a high level uicomponent like the UIImageView
The number one reason your code is slow right now is because you're making a network call on the main thread. This blocks an UI from being updated and prevents the OS from handling events (such as taps).
As already suggested, I recommend ASIHTTPRequest. Combine asynchronous requests with an NSOperationQueue with a smaller concurrency count (say, 2) for the image requests, caching, and reloading the rows when images come in (on the main thread) (also only reloading the row if its currently visible). You can get a smooth scrolling table view.
I have an example of this on github here: https://github.com/subdigital/iphonedevcon-boston
It's in the Effective Network Programming project and includes a set of projects that progressively improve the experience.
Download the images before you load the tableView, and store them in an NSArray. Then when the cellForRowAtIndexPath method is called, show a loading image until the image is in the array.