0

I've been banging my head against the wall on this one and searched far and wide for a solution to no avail:

I have a large array of data pulled from the web and I'm using Loren Brichter's ABTableViewCell to make it run smoothly by drawing everything inside of the contentView of each cell to avoid UILabels and UIImageViews slowing scrolling down.

This works great for displaying text, but I run into a problem with images because of the time it takes to download them. I can't seem to find a way to force the contentView of each cell displayed to redraw itself once the corresponding image has been downloaded. I must point out I am not drawing labels and imageViews, but just the contentView in order to save memory.

Right now the table behaves like this:

  • Load: text displayed, no images
  • Scroll up or down: images finally show up once the cells move off screen

A sample project is here

Code:

ABTableViewCell.h

@interface ABTableViewCell : UITableViewCell
{
    UIView *contentView;
}

ABTableViewCell.m

- (void)setNeedsDisplay
{
    [contentView setNeedsDisplay];
    [super setNeedsDisplay];

}

- (void)drawContentView:(CGRect)r
{
    // subclasses implement this
}

TableCellLayout.h

#import "ABTableViewCell.h"

@interface TableCellLayout : ABTableViewCell {

}

@property (nonatomic, copy) UIImage *cellImage;
@property (nonatomic, copy) NSString *cellName;

TableCellLayout.m

#import "TableCellLayout.h"

@implementation TableCellLayout

@synthesize cellImage, cellName;

- (void)setCellName:(NSString *)s
{
    cellName = [s copy];
    [self setNeedsDisplay];
}

- (void)setCellImage:(UIImage *)s
{
    cellImage = [s copy];
    [self setNeedsDisplay];
}

- (void)drawContentView:(CGRect)r
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextFillRect(context, r);

    [cellImage drawAtPoint:p];
    [cellName drawAtPoint:p withFont:cellFont];
}

TableViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    TableCellLayout *cell = (TableCellLayout *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if(cell == nil)
    {
        cell = [[TableCellLayout alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    cell.cellName = [[array valueForKey:@"name"] objectAtIndex:indexPath.row];
    cell.cellImage = [UIImage imageNamed:@"placeholder.png"]; // add a placeholder    

    NSString *imageURL = [[array valueForKey:@"imageLink"] objectAtIndex:indexPath.row];
    NSURL *theURL = [NSURL URLWithString:imageURL];

    if (asynchronousImageLoader == nil){
        asynchronousImageLoader = [[AsynchronousImages alloc] init];
    }
       [asynchronousImageLoader loadImageFromURL:theURL];

        cell.cellImage = asynchronousImageLoader.image;

return cell;
}

This is the final method the AsynchronousImageLoader calls once the image is prepared:

- (void)setupImage:(UIImage*)thumbnail {
    self.image = thumbnail;
        [self setNeedsLayout];

}

I just need the correct way to tell my visible cells to redraw themselves once the row's image has been downloaded. I imagine I should be putting something in that final method (setupImage)--but I can't seem to get it working the way it should. Thoughts? Many thanks!

Final edit: the solution

Right, so as suspected, the problem was that visible cells weren't being told to redraw and update to the downloaded image once the call was complete.

I used the help provided by the answers below to put together a solution that works well for my needs:

Added a callback in the final method that the asynchronous image downloader calls:

AsyncImageView.m

 - (void)setupImage:(UIImage*)thumbnail {

      self.cellImage = thumbnail;
[cell setNeedsDisplay];

}

Note: I also set a local placeholder image in the initialization of the image downloader class just to pretty things up a bit.

Then in my cellForRowAtIndexPath:

NSURL *theURL = [NSURL URLWithString:imageURL];

AsyncImageView *aSync = [[AsyncImageView alloc] initWithFrame:CGRectMake(0, 0, 20, cell.bounds.size.height)];

[aSync loadImageFromURL:theURL];
cell.cellImageView = aSync;

return cell;

There may have been one or two other tweaks, but those were the major problems here. Thanks again SO community!

  • did you solve this? I updated my answer below 2 days ago with code which I tested works. Please would you check the details and confirm it's solved. Thanks! – ikuramedia Aug 19 '12 at 07:58
  • Hi, yes, the community has been awesome once more. I'll accept answers and write a brief edit to show what I did to get it all working. Thanks for the help! –  Aug 19 '12 at 07:59
  • Great to hear, happy to help! – ikuramedia Aug 19 '12 at 08:02

5 Answers5

0

You can used the Apple TableView Lazy Loading. They have sample codes that download images asynchonously. See link below

Apple LazyTableImages

On your end in AsynchronousImages class you can add an attribute NSIndexPath and the delegate on AsynchronousImages should be change. See the code below

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    TableCellLayout *cell = (TableCellLayout *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if(cell == nil)
    {
        cell = [[TableCellLayout alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    cell.cellName = [[array valueForKey:@"name"] objectAtIndex:indexPath.row];
    cell.cellImage = [UIImage imageNamed:@"placeholder.png"]; // add a placeholder    

    NSString *imageURL = [[array valueForKey:@"imageLink"] objectAtIndex:indexPath.row];
    NSURL *theURL = [NSURL URLWithString:imageURL];


    AsynchronousImages *asynchronousImageLoader = [[AsynchronousImages alloc] init];
    asynchronousImageLoader.indexPath = indexPath;
    [asynchronousImageLoader loadImageFromURL:theURL];
    return cell;
}

//Delegate should be
- (void)setupImage:(UIImage*)thumbnail index:(NSIndexPath*) indePath {
    TableCellLayout *cell = (TableCellLayout *) [tableView cellForRowAtIndexPath:indexPath];
    if(cell) {
      cell.cellImage = thumbnail;
      [cell setNeedsDisplay];
    }
}
DLende
  • 5,162
  • 1
  • 17
  • 25
  • I know of Apple's example, but I have everything working now as it is. I really think it's just a matter of being able to force visible cells to redraw once the images have completed downloading. Do you have any ideas about how to achieve that? Thanks for the reply! –  Aug 16 '12 at 04:39
  • but your code is complicated since when the delegate setupImage trigger you can't get the right row of the cell to display the image. You can see the apple sample code to have hint how they made it. – DLende Aug 16 '12 at 04:46
  • You did not `released` (or `autoreleased`) `asynchronousImageLoader`. – geekay Dec 13 '12 at 06:48
0

verify UIImage creation and setting of the UIImageView's image property happen on the main thread. there is no reason setting the image view's image should not invalidate its rect if visible.

also confirm that your loads are cancelled correctly, if you are reusing cells.

justin
  • 104,054
  • 14
  • 179
  • 226
  • Hey Justin, thanks for this. Helped point me in the right direction in the end! –  Aug 19 '12 at 08:06
0

Use AsyncImageView in place of uiimageview

Lithu T.V
  • 19,955
  • 12
  • 56
  • 101
  • Lithu, I am utilising AsyncImageView. The images are downloaded, but do not appear to the user until the visible cells are moved off screen (and then back on). I'm guessing this is an issue of forcing the cell redraw manually. Thoughts? –  Aug 16 '12 at 06:20
  • chk the sample tutorial coming with the source..it explains well how to make it wrk properly.in tableview cellforRow method, set the imageurl property of asynchimage variable.. it works fine for me..hope it helps – Lithu T.V Aug 16 '12 at 08:42
  • tht class provide not only category but also the asyncimageview subclass of uiimageview – Lithu T.V Aug 16 '12 at 08:44
  • if u can save the data u can load it in customcell drawrect method i think – Lithu T.V Aug 16 '12 at 08:46
0

You can tell your tableView to reloadData... or a slightly more refined reload:

  [self.tableView reloadRowsAtIndexPaths:self.tableView.indexPathsForVisibleRows withRowAnimation:UITableViewRowAnimationNone];

Edit to Add (after looking at OP's source):

I've looked through your project, and the problem is with your architecture. You're misusing AsyncImageView because you're only using it to asynchronously load your image - whereas it is designed to both load, and display the image. This is why it has no 'callback' function to let you know when the data has been retrieved.

You would be better off replacing your CellLayout's image property with a UIImageView property instead. (Note that UIImageView is more efficient at drawing than image drawAtPoint anyway).

So:

  1. Change your CellLayout class to use an UIImageView property instead of UIImage
  2. Change your cellForRowAtIndexPath to set the AsyncImageView as a property directly on your Cell.

If you want to support placeholders, that should be added to your AsyncImageView class - so that it knows what to display while downloading the content.

cellForRowAtIndexPath:

NSURL *theURL = [NSURL URLWithString:imageURL];

AsyncImageView *aSync = [[AsyncImageView alloc] initWithFrame:CGRectMake(0, 0, 20, cell.bounds.size.height)];

[aSync loadImageFromURL:theURL];
cell.cellImageView = aSync;

return cell;

CellLayout.h

@property (nonatomic, strong) UIImageView *cellImageView;

CellLayout.m

- (void)setCellImageView:(UIImageView *)s
{
    [cellImageView removeFromSuperview];
    cellImageView = s;
    [self addSubview:cellImageView];
}
ikuramedia
  • 6,038
  • 3
  • 28
  • 31
  • Hmm, I've run through everything laid out in the answers and haven't been able to remedy this. I know I'm just starting out, so I'm probably missing something silly. I put together a sample [project](https://www.yousendit.com/download/TEhXU2VndWNrUm1xV2NUQw). You're probably busy, but if you have a few spare minutes, I'd be very thankful if you could help end this mystery! –  Aug 16 '12 at 11:02
0

Make sure that you are updating the cell image in the Main Thread. UI updates only appear if done there, which is why you only see the update when you touch & scroll.

if(cell) {

    dispatch_async(dispatch_get_main_queue(), ^{

        cell.cellImage = thumbnail;
        [cell setNeedsDisplay];

    });

}

[EDIT]

You need the cell to be the delegate of the image loader and own the async redraw mechanism.

......
    AsynchronousImages *asynchronousImageLoader = [[AsynchronousImages alloc] init];
    asynchronousImageLoader.delegate = cell;
    [asynchronousImageLoader loadImageFromURL:theURL];

    return cell;
}

And place the delegate call back code in the cell implementation.

- (void)setupImage:(UIImage*)thumbnail {

      self.cellImage = thumbnail;

}
Warren Burton
  • 17,451
  • 3
  • 53
  • 73
  • Hey Warren, I've tried fiddling with that a bit but haven't gotten anything to work. I'm running on the main thread (I never switch to the background) so that isn't it. I've [uploaded](https://www.yousendit.com/download/TEhXU2VndWNrUm1xV2NUQw) a barebones sample project if you would be so kind to take a few minutes out of your day to skim it, I'd be eternally grateful. –  Aug 16 '12 at 10:58
  • The actual problem here is that what you have here wont work, ever. You shouldnt ever call cellForRowAtIndexPath: and expect to get the cell which is displayed currently. See EDIT – Warren Burton Aug 17 '12 at 11:42