iOS lazy-loading of table images

30,078

Solution 1

You can achieve lazy loading in your tableview by following these steps :

In your Class.h, put:

NSMutableDictionary *Dict_name;
BOOL isDragging_msg, isDecliring_msg;

Now In Class.m file, put this code in view did load:

Dict_name = [[NSMutableDictionary alloc] init];

In cellForRowAtIndexPath:

if ([dicImages_msg valueForKey:[[msg_array objectAtIndex:indexPath.row] valueForKey:@"image name or image link"]]) { 
    cell.image_profile.image=[dicImages_msg valueForKey:[[msg_array objectAtIndex:indexPath.row] valueForKey:@"image name or image link"]];
}
else
{
    if (!isDragging_msg && !isDecliring_msg)
    {
        [dicImages_msg setObject:[UIImage imageNamed:@"Placeholder.png"] forKey:[[msg_array objectAtIndex:indexPath.row] valueForKey:@"image name or image link"]];
        [self performSelectorInBackground:@selector(downloadImage_3:) withObject:indexPath];
    }
    else
    {
        cell.image_profile.image=[UIImage imageNamed:@"Placeholder.png"];
    }
}

And for the download image the function is:

-(void)downloadImage_3:(NSIndexPath *)path{
    NSAutoreleasePool *pl = [[NSAutoreleasePool alloc] init];

    NSString *str=[here Your image link for download];

    UIImage *img = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:str]]]; 

    [dicImages_msg setObject:img forKey:[[msg_array objectAtIndex:path.row] valueForKey:@"image name or image link same as cell for row"]];

    [tableview performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];

    [pl release];
}

And at last put these methods in your class:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    isDragging_msg = FALSE;     
    [tableview reloadData];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    isDecliring_msg = FALSE;
    [tableview reloadData]; 
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    isDragging_msg = TRUE;
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
    isDecliring_msg = TRUE; 
}

Solution 2

Updated answer:

The standard Apple LazyTableImages suffers from a few flaws:

  1. It won't try to retrieve the images until the scrolling finished. I understand why they chose to do that (probably don't want to retrieve images that might be scrolling away), but this causes the images to appear more slowly than necessary.

  2. It does not constrain the number of concurrent requests to some reasonable number. Yes, NSURLConnection will automatically freeze requests until the maximum number of requests falls to some reasonable number, but in a worst case scenario, you can actually have requests time out, as opposed to simply queueing properly.

  3. Once it starts to download the image, it won't cancel it even if the cell subsequently scrolls off of the screen.

The simple fix is to retire IconDownloader and all of the scrolling/decelerating logic and just use the UIImageView category from either SDWebImage or AFNetworking.

If you're going to fix this yourself, it takes a little work to get all of this right. But here is a rendition of LazyTableImages that uses NSOperationQueue to ensure that we (a) limit concurrent requests; and (b) allows us to cancel requests. I confess I prefer the above UIImageView category implementations, better, but I tried to stay with the structure of Apple's original LazyTableImages, while remedying the flaws I've enumerated above.

Solution 3

Assuming your UITableViewCell has an url property and a UIImage property for the image (plus a property or static value for the queue:

- (void) setThumbnailUrlString:(NSString *)urlString
{
    thumbnailUrlString = urlString;

    NSURL *url = [NSURL URLWithString:urlString];

    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    if ( queue == nil )
    {
        queue = [[NSOperationQueue alloc] init];
    }
    [NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse * resp, NSData     *data, NSError *error) 
    {
        dispatch_async(dispatch_get_main_queue(),^ 
                       {
                            if ( error == nil && data )
                            {
                                UIImage *urlImage = [[UIImage alloc] initWithData:data];
                                thumbnail.image = urlImage;
                            }
                       });
    }];
}

The issue with your implementation is that your image loading method is probably occurring in the main thread. You need to load the image asynchronously and then update the image in the main thread.

To be correct the block should check to see if the response's url matches the requested url if the cell's image loaded too late before the cell was reused for a different image.

Share:
30,078
Rameez Hussain
Author by

Rameez Hussain

Updated on December 26, 2020

Comments

  • Rameez Hussain
    Rameez Hussain over 3 years

    I have successfully implemented lazy-loading of images in my app using the example provided by apple here. The thing is that, I want the image to load as I am scrolling, but the image loads into the cell only when I finish dragging (Release my finger from the screen. Until then the cell remains empty). I have modified the code as follows:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    
        //NSLog(@"cell for row at indexpath, size - %f, current - %d", flowTable.contentSize.height, self.current);
    
        NSString *CellIdentifier = @"FlowCell";
    
        MyFlowCell *cell = (MyFlowCell *)[self.flowTable dequeueReusableCellWithIdentifier:CellIdentifier];
    
        if (cell == nil) {
    
            NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"FlowCell" owner:nil options:nil];
           // cell = [nib objectAtIndex:0];
    
            for(id currentObject in nib)
    
            {
    
            if([currentObject isKindOfClass:[MyFlowCell class]])
    
            {
                cell = (MyFlowCell *)currentObject;
    
                break;
            }
            }
        }
    
        ImageDetails *rowItem = [self.imageData objectAtIndex:indexPath.row];
    
        if (!rowItem.mainImage)
        {  
           // if (self.flowTable.dragging == NO && self.flowTable.decelerating == NO)
           // {
    
                [self startIconDownload:rowItem forIndexPath:indexPath];
    
            //}
    
            cell.mainImage.backgroundColor = [UIColor grayColor];
    
        }
        else
        {
    
                cell.mainImage.image = rowItem.mainImage;
    
        }
        }
    
    
        return cell;
    }
    
    
    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
    {
        if (!decelerate)
        {
          //  [self loadImagesForOnscreenRows];
        }
    }
    
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
    {
        //[self loadImagesForOnscreenRows];
    }
    

    Moreover, I found out that in my ImageDownloader class, the NSURLConnection doesn't even receive the response until I release my finger from the screen. I have been trying to find out why this is happening, but I haven't been successful so far. Please tell me if I've been missing something.

  • Rob
    Rob over 11 years
    Yes, that demonstrates one solid technique (though using reloadData is an inefficient way to update the cell).
  • Rameez Hussain
    Rameez Hussain over 11 years
    Hey guys. Thanks a LOT for all your answers. Each one helped me in its own way to solve the problem. I mostly used the concept of this post, though. So just for the sake of it, I will accept this one. :)
  • Rob
    Rob almost 11 years
    My original answer employed Grand Central Dispatch to simplify the Apple LazyTableImages code. But it didn't solve many of the structural problems of this original implementation, so my revised answer addresses those points.
  • John Riselvato
    John Riselvato over 10 years
    queue is a NSOperationQueue btw
  • ʞᴉɯ
    ʞᴉɯ over 10 years
    The linked GitHub project does not resolve the waiting for end scrolling. The SDWebImage does instead.
  • Rob
    Rob over 10 years
    BTW, you don't need to create this operation queue. You can just use [NSOperationQueue mainQueue] as the queue parameter to sendAsynchronousRequest. The request still happens asynchronously, but the completion block will be called on the main queue, thus eliminating the need for that dispatch_async to the main queue. The queue parameter of sendAsynchronousRequest is the queue for the completion block, not for the request itself.