UITableView On Screen Image Download Lazy Loading - ios

First of all this is not a duplicate. I have seen some identical questions but they didn't help me as my problem varies a little bit.
Using the following code i am download the images asynchronously in my project.
{
NSURL *imageURL = [NSURL URLWithString:imageURLString];
[self downloadThumbnails:imageURL];
}
- (void) downloadThumbnails:(NSURL *)finalUrl
{
dispatch_group_async(((RSSChannel *)self.parentParserDelegate).imageDownloadGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *tempData = [NSData dataWithContentsOfURL:finalUrl];
dispatch_async(dispatch_get_main_queue(), ^{
thumbnail = [UIImage imageWithData:tempData];
});
});
}
Due to the logic of the program, i have used the above code in files other than the tableview controller which is showing all the data after getting it from the web service.
PROBLEM: On screen images does not show up until i scroll. The off screen images are refreshed first. What can i do to solve my problem.
Apple's lazy loading project is using scrollViewDidEndDragging and scrollViewDidEndDecelerating to load the images but the project is way too big to understand plus my code is in files other than the tableview controller.
NOTE: Kindly do not recommend third party libraries like SDWebImage etc.
UPDATE: As most of people are unable to get the problem, i must clarify that this problem is not associated with downloading, caching and re-loading the images in tableview. So kindly do not recommend third party libraries. The problem is that images are only showing when the user scrolls the tableview instead of loading the on screen ones.
Thanks in advance

I think what you have to do is:
display some placeholder image in your table cell while the image is being downloaded (otherwise your table will look empty);
when the downloaded image is there, send a refresh message to your table.
For 2, you have two approaches:
easy one: send reloadData to your table view (and check performance of your app);
send your table view the message:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
Using reloadRowsAtIndexPaths is much better, but it will require you to keep track of which image is associated to which table row.
Keep in mind that if you use Core Data to store your images, then this workflow would be made much much easier by integrating NSFetchedResultController with your table view. See here for an example.
Again another approach would be using KVO:
declare this observe method in ItemsViewCell:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:#"thumbnail"]) {
UIImage* newImage = [change objectForKey:NSKeyValueChangeNewKey];
if (newImage != (id)[NSNull null]) {
self.thumbContainer.image = newImage;
[self.thumbContainer setNeedsLayout];
}
}
}
then, when you configure the cell do:
RSSItem *item = [[channel items] objectAtIndex:[indexPath row]];
cell.titleLabel.text = [item title];
cell.thumbContainer.image = [item thumbnail];
[item addObserver:cell forKeyPath:#"thumbnail" options:NSKeyValueObservingOptionNew context:NULL];
By doing this, cell will be notified whenever the given item "thumbnail" keypath changes.
Another necessary change is doing the assignment like this:
self.thumbnail = [UIImage imageWithData:tempData];
(i.e., using self.).
ANOTHER EDIT:
I wanted to download and load the images just like in the LazyTableImages example by Apple. When its not decelerating and dragging, then only onscreen images are loaded, not all images are loaded at once.
I suspect we are talking different problems here.
I thought your issue here was that the downloaded images were not displayed correctly (if you do not scroll the table). This is what I understand from your question and my suggestion fixes that issue.
As to lazy loading, there is some kind of mismatch between what you are doing (downloading the whole feed and then archiving it as a whole) and what you would like (lazy loading). The two things do not match together, so you should rethink what you are doing.
Besides this, if you want lazy loading of images, you could follow these steps:
do not load the image in parser:foundCDATA:, just store the image URL;
start downloading the image in tableView:cellForRowAtIndexPath: (if you know the URL, you can use dataWithContentOfURL as you are doing on a separate thread);
the code I posted above will make the table update when the image is there;
at first, do not worry about scrolling/dragging, just make 1-2-3 work;
once it works, use the scrolling/dragging delegate to prevent the image from being downloaded (point 2) during scrolling/dragging; you can add a flag to your table view and make tableView:cellForRowAtIndexPath: download the image only if the flag says "no scrolling/dragging".
I hope this is enough for you to get to the end result. I will not write code for this, since it is pretty trivial.
PS: if you lazy load the images, your feed will be stored on disk without the images; you could as well remove the CGD group and CGD wait. as I said, there is not way out of this: you cannot do lazy loading and at the same time archive the images with the feed (unless each time you get a new image you archive the whole feed). you should find another way to cache the images.

Try using SDWebImage, it's great for using images from the web in UITableViews and handles most of the work for you.

The best idea is caching the image and use them. I have written the code for table view.
Image on top of Cell
It is a great solution.

Try downloading images using this code,
- (void) downloadThumbnails:(NSURL *)finalUrl
{
NSURLRequest* request = [NSURLRequest requestWithURL:finalUrl];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data, NSError * error)
{
if (!error)
{
thumbnail = [UIImage imageWithData:data];
}
}];
}

Related

How would I design an asynchronous image downloader for my UITableView that prioritizes downloads depending on location in table view?

In my app, I receive a list of image URLs to use as thumbnails in my table view. The tableview has a small amount of items, approximately 30, so I want to load all the thumbnails immediately (instead of when they become visible, as they undoubtedly will become visible and I want them fully loaded then).
I want to prioritize the image downloads by index path, so the first index path has priority over the second, which has priority over the third, etc.
However, if I suddenly jump to the end of the table view (which shows, say, index paths 24-29) I want the images for those index paths to become highest priority, so if they jump at the very beginning they won't have to wait for all the others to download first.
How would I go about designing something like this? If it's possible with SDWebImage that'd be great, as I'm comfortable with that, but if not I'm more than fine with creating something from scratch with NSURLSession. (I've looked at SDWebImage, and the SDWebImagePrefetcher looks promising, but doesn't allow priority changing from what I've seen.)
I recently had a similar problem, but didn't need to have a priority to load. I can't think of how to change the priority of what is loaded unless you do the loading of thumbnails before loading the tableview. Tableviews load cells as they are needed.
I can think of two options:
If you want all the data loaded and ready before the tableview is even loaded, preemptively load the data in an earlier view controller and save it to be opened in the view controller containing your table view. Then no requests will have to be made by your tableview and everything will appear seamlessly.
The tableview sends each thumbnail request asynchronously and updates the cell as the images arrive on the main thread. You can then cache or save the images after they arrive. But the images won't appear instantly, generally a split second later.
I did option two using NSCache. The value for key "avatar" is the URL of the thumbnail image. This code is located in my - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
if (![teamMember[#"avatar"] isEqualToString:#""]) {
// check for cached image, use if it exists
UIImage *cachedImage = [self.imageCache objectForKey:teamMember[#"avatar"]];
if (cachedImage) {
cell.memberImage.image = cachedImage;
}
//else retrieve the image from server
else {
NSURL *imageURL = [NSURL URLWithString:teamMember[#"avatar"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
// if valid data, create UIImage
if (imageData) {
UIImage *image = [UIImage imageWithData:imageData];
// if valid image, update in tableview asynch
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
TeamCommitsCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
// if valid cell, display image and add to cache
if (updateCell) {
updateCell.memberImage.image = image;
[self.imageCache setObject:image forKey:teamMember[#"avatar"]];
}
});
}
}
});
}
}

Image View is Causing Awkward Hang?

I've been working on this for a couple days now (off & on) and I'm not exactly sure why this isn't working, so I'm askin you pros at SOF for some insight.
NewsItem.m
On my first view controller, I'm reading from a JSON feed which has 10+ items. Each item is represented by a NewsItem view which allows for a title, body copy, and a small image. The UIImageView has an IBOutlet called imageView. I'm loading the image for my imageView asynchronously. When the image is loaded, I'm dispatching a notification called IMAGE_LOADED. This notification is only picked up on the the NewsItemArticle
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//this will start the image loading in bg
dispatch_async(concurrentQueue, ^{
NSData *image = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:self.imageURL]];
//this will set the image when loading is finished
dispatch_async(dispatch_get_main_queue(), ^{
[self.imageView setAlpha:0.0];
self.image = [UIImage imageWithData:image];
[self.imageView setImage:self.image];
[UIView animateWithDuration:0.5 animations:^{
[self.imageView setAlpha:1.0];
}];
if(self)
[[NSNotificationCenter defaultCenter] postNotificationName:IMAGE_LOADED object:self];
});
});
NewsItemArticle.m
When a user taps on a NewsItemView then I load a new controller which is a scroll view of several NewsItemArticle views inside a scrollview. A NewsItemArticle will listen for IMAGE_LOADED and if it is decided the current notification has an image for this particular article, it will use the same image for it's own reference like so:
- (void)handleImageLoaded:(NSNotification *)note
{
if([note.object isEqual:self.cell]) {
// this next line is hanging the app. not sure why.
[self.imageView setImage:self.cell.image];
[self.activityViewIndicator removeFromSuperview];
}
}
So essentially:
I'm using an asynchronous load on my first image reference
I'm using notifications to let other parts of the app know and image was loaded
The app hangs when the existing image is reference to a second UIImageView
If I comment out the suspect line, the app never hangs. As it its, my app hangs until all the images are loaded. My thoughts are:
This is a network threading conflict (not likely)
This is a GPU threading conflict (perhaps during a resize to the container view's size?)
Has anyone seen anything like this before?
For lazy loading of table view images there are few good options available. Can make use of them in your design to save time and avoid efforts to reinvent the wheel.
1. Apple lazy loading code --link
2. SDWebImage --link
SDWebImage will provide you a completion handler/block where you can use the notification mechanism to notify other modules of your application.
Cheers!
Amar.

Memory Leak Downloading image from server

I have a paged slider view with an image on each page. I'm using NSOperationQueue to help me download the images from the server while the program is running. The NSOperationQueue is used to call the following method,
-(NSData *)imageWith:(NSString *)imageName
{
NSString *imagePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:imageName];
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
if (!imageData) {
imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:[[NSString stringWithFormat:#"%#/%#", picsURL,imageName] stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding]]];
if (imageData) {
[imageData writeToFile:imagePath atomically:YES];
}
}
return imageData;
}
and then I use the main thread to display the downloaded image on the scrollerview:
[self performSelectorOnMainThread:#selector(loadPic:) withObject:[NSArray arrayWithObjects:[self imageWith:[picsNames objectAtIndex:imageView.tag]], [NSString stringWithFormat:#"%d", imageView.tag], nil] waitUntilDone:YES];
which calls the following method:
-(void)loadPic:(NSArray *)imageAndTagArray
{
if (imageAndTagArray.count) {
//loading the image to imageview
UIImageView *imageView = (UIImageView *)[scrollView viewWithTag:[[imageAndTagArray objectAtIndex:1] intValue]];
imageView.image = [UIImage imageWithData:((NSData *)[imageAndTagArray objectAtIndex:0])];
//stopping the indicator
[((UIActivityIndicatorView *)[imageView viewWithTag:ACTIVITY_INDICATOR_TAG]) stopAnimating];
}
}
Everything works fine for the first 60 images, but after that I receive a Memory Warning and after about 100 images the app crashes.
I have been spending so much time on this and I can't figure out what to do. I've used Instruments and it doesn't detect any leak. I've also used Analyze and that did show anything either.
EDIT:
If I replace the imageWith: method definition with the following definition I still get the warnings, where 5.jpg is a local image.
-(NSData *)imageWith:(NSString *)imageName
{
return UIImagePNGRepresentation([UIImage imageNamed:#"5.jpg"]);
}
Let me tell you more about the situation.
When the app starts I have a view with a paged scrollview inside it that contains 9 images per page. the scrollview uses the nsoperationqueue to load images which calls the imageWith: method.
when the user taps on any of the images a second view opens with a full display of the selected image. this second view also has a scroll view that contains the same images as the first view but with full display, meaning 1 image per page.
when you are on the second view and scrolling back and forth the app crashes after loading about 60 images.
It also crashes if say it loads 50 images and then you tap on the back button and go to the first view and then tap on another image and go to the second view and load about 10 images.
It sounds like you're holding too many images in memory. When you open the second view, it's reloading the images again from disk, until you end up with two copies of all the images.
The UIImage class may be able to help you with this memory management. In its reference page, it mentions that it has the capability to purge its data in low-memory situations and then reload the file from disk when it needs to be drawn again. This might be your solution.
However, as you're creating the image from an NSData read from disk, the UIImage will probably not be able to purge its memory - it won't know that your image is simply stored on the disk, so it can't throw away the data and reload it later.
Try changing your "imageWith" method to create a UIImage (via imageWithContentsOfFile) from the file URL on the disk just before it returns, and return the UIImage rather than returning the intermediate NSData. That way, the UIImage will know where on disk its image source came from and be able to intelligently purge/reload it as memory becomes constrained on the device.

iOS close background thread

In my ViewController, I'm downloading a number of png files off the internet and adding them to UIImageViews, which I add to a UIScrollView.
However, if the user presses the back button in the navigation bar, I noticed that the background thread is still continuing to download those files. Also, I noticed huge memory spikes as a result, and I don't think my objects are properly getting released.
How can I close all threads when the user presses back in the navigation bar?
xcode 4.2.1, ARC
Thanks!
EDIT -- the following is in a for loop
NSURL* url = [[NSURL alloc] initWithString:[#"someurl"]];
NSData* imageData = [[NSData alloc]initWithContentsOfURL:url];
UIImage* image = [[UIImage alloc] initWithData:imageData];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame = aFrame;
[self.screenshots addSubview:imageView]
You shouldn't be doing any downloading in your view controller. That's the source of the problem. Move your data into model classes. Do all of your downloading and data management there. The view controllers are just responsible for pulling data from the model and giving it to the views. View controllers are not responsible for fetching data from the network. As you've discovered, they can vanish at any time.
You don't want to abort downloads and then restart them every time someone moves between screens. You want to let the model classes know that some data is needed (because the user came to a screen), and then you want the model classes to post a notification (or provide a KVO change notification) that lets the view controllers know that new data is read.
You may want to consider NSOperationQueue and add new download operations via. NSURLConnection and add a supply your NSOperationQueue. You can have more then one source downloading at once and cancel all operations at once once you need to. Also you can set things like how many to download at a time.
Try this:
in your for loop, make a condition that would stop the downloads when you don't want them anymore:
declare class variable or properly
BOOL stopDownloading;
set it to false on when you start the downloads:
stopDownloading = NO;
modify your for loop to stop when you set it to NO:
if (!stopDownloading) {
NSURL* url = [[NSURL alloc] initWithString:[#"someurl"]];
NSData* imageData = [[NSData alloc]initWithContentsOfURL:url];
UIImage* image = [[UIImage alloc] initWithData:imageData];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame = aFrame;
[self.screenshots addSubview:imageView];
}
so now, when back button is pressed, set
stopDownloading = YES;
it will stop all further downloads, however due to your setup, i don't immediately see a way to stop the one that is currently downloading (only subsequent ones).
To solve this problem, i would create a NSOperationQueue and add NSOperations for each download.
Or even simpler, use some existing open source solutions like AFNetworking. specifically these two:
AFImageRequestOperation A subclass of AFHTTPRequestOperation for downloading and processing images.
UIImageView+AFNetworking Adds methods to UIImageView for loading remote images asynchronously from a URL.

What's the most efficient way to handle a UIButton Photo Grid in a UITableView?

I have an iOS app I'm working on that grabs a bunch of photo URLs from a MySQL database with a JSON request. Once I have these photos and related information, I use it to populate the datasource for a UITableView. I want to create a grid of UIButtons, made out of photos, 4 per row. This current code works, however it is wildly slow and my phone / simulator freezes right up as I scroll through the table. Tables with only a couple rows work fine, but once I reach 10 or more rows it slows right down and near crashes. I'm new to iOS and objective-c, so I'm assuming it's an inefficiency in my code. Any suggestions? Thanks!!
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger row = [indexPath row];
static NSString *CompViewCellIdentifier = #"CompViewCellIdentifier";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier: CompViewCellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CompViewCellIdentifier] autorelease];
}
// The photo number in the photos array that we'll need to start off with.
NSUInteger photoNumber = (row * 4);
// Assemble the array of all 4 photos we'll need for this table row (for this cell).
NSMutableArray *rowPhotos = [[[NSMutableArray alloc] initWithObjects:[self.photos objectAtIndex:photoNumber], nil] retain];
NSInteger counter = 1;
while ([self.photos count] > photoNumber+counter && counter<4) {
[rowPhotos addObject:[self.photos objectAtIndex:photoNumber+counter]];
counter = counter+1;
}
NSLog(#"The rowPhotos array: %#", rowPhotos);
for (int i=0; i<[rowPhotos count]; i++) {
// Set which photo we're dealing with for this iteration by grabbing it from our rowPhotos array we assembled. Use i as the index.
NSDictionary *photoRow = [[NSDictionary alloc] initWithDictionary:[rowPhotos objectAtIndex:i]];
// Get the photo.
NSString *photoPath = [[NSString alloc] initWithFormat:#"http://localhost/photorious%#", [photoRow objectForKey:#"path"]];
NSURL *url = [NSURL URLWithString: photoPath];
[photoPath release];
UIImage *cellPhoto = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:url]];
// Figure out the container size and placement.
int xCoordinate = ((i*70)+8*(i+1));
CGRect containerRect = CGRectMake(xCoordinate, 0, 70, 70);
// Create the Button
UIButton *cellPhotoButton = [UIButton buttonWithType:UIButtonTypeCustom];
[cellPhotoButton setFrame:containerRect];
[cellPhotoButton setBackgroundImage:cellPhoto forState:UIControlStateNormal];
[cellPhotoButton setTag:(NSInteger)[photoRow objectForKey:#"id"]];
// Add the button to the cell
[cell.contentView addSubview:cellPhotoButton];
// Add the action for the button.
[cellPhotoButton addTarget:self
action:#selector(viewPhoto:)
forControlEvents:UIControlEventTouchUpInside];
[cellPhoto release];
}
[rowPhotos release];
return cell;
}
This is slow because you do everything in tableView:cellForRowAtIndexPath:.
tableView:cellForRowAtIndexPath: Is called really ofter, especially each time a cell need to be displayed in your tableview, which includes when your are scrolling your tableView. Thus this method needs to be fast, and non-blocking (especially don't do synchronous downloads!)
Moreover your don't use the reusability of your tableview cells correctly. This drastically decrease performance as you recreate the content (subviews) for each cell each time.
When your cell is reused from a previous one (see it as being "recycled"), you must NOT redo everything, especially you must not re-add every subviews as there already are in the cell itself, as it has been reused and is not a clean new one!
Instead, when dequeueReusableCellWithIdentifier: returns a cell (= an old cell previously created but not used anymore so you can "recycle"/reuse it), you should only change what differs from cell to cell. In your example, typically you will only change the 4 images displayed, but don't recreate the UIImageView, neither add them to as a subview (as these subviews already exists) nor reaffect the target/action.
You only need to create the UIImageView, add them a target/action, set their frame and add them as a subview when your are creating a brand new cell, with alloc/initWithReuseIdentifier:/autorelease.
Moreover, you are fetching your images from the network directly in your tableView:cellForRowAtIndexPath:, and synchronously in addition (which means it blocks your application until it finished downloading the image from the net!!).
Do an asynchronous download instead, way before your tableView:cellForRowAtIndexPath: (when your app is loaded for example) and store them locally (in an NSArray, or sthg similar for example), and only fetch the local, already downloaded image in your tableView:cellForRowAtIndexPath:.
The thing you are trying to do is not the greatest idea to begin with if you are new to iOS programming. What you wanna do may seem easy, but it implies concepts like asynchronous downloads, MVC design of your app and prefetching the images from the net in your model before displaying them in your view, provide a way to update the tableview when the download is done, and the basic concepts of cell reuse in tableviews.
DO read the TableView Programming Guide before going further. It explains it in details and it really worth reading.
Also consult Apple's LazyTableImages sample code which explains how to load images in a tableview lazyly (meaning loading images asynchronously when they are needed), and the URL Loading Programming Guide which explains how to do asynchronous downloads of data.
These guides and samples are really worth reading if you want to do what you explain. There are also a lot of classes to do Grid Views on the net, one of them being my work (OHGridView), but you need to understand basics explained above and in the mentioned guides first before going further.

Resources