markjnet

UITables with Downloaded Images – Easy Asynchronous Code

Readers… do look through the comments if you plan to use this code, other people have posted improvements. Thanks for all the great feedback everyone. MJ

postcards-screen1The app ‘Postcards’ from my iPhone developer training class is a utility app for quickly sending a customized postcard, and one thing that makes it super easy is that you can grab pictures from Flickr to include in the postcard design. Postcards makes simple HTTP calls Flickr’s REST API to download public domain images and displays them in a UITableView for the user to pick from. Cocoa Touch makes this all simple and easy to code, and my first development version used synchronous calls to get the images by using NSData dataWithContentsOfURL:

.

.

NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urls]];

Making synchronous calls to remote web servers from the thread that’s running the apps GUI is of course a bad idea that results in a laggy UI and unsatisfied users. Using synchronous calls in UITableView cellForRowAtIndexPath to load all the images results in a problem six times worse (for 6 rows on the screen) and makes scrolling basically broken as the table won’t scroll until it has the next cell, which it can’t get while the app is waiting for an image to download. Then imagine that on the Edge network! Obviously we need something multi-threaded that can load the images in parallel in the background and update the UI as they finish downloading.

Multi-threaded programming is hard and should be avoided whenever possible, and in this case Cocoa’s beautiful design came to my rescue:

UIView heirachy + URL loading system + delegate design = multi-threaded image loading with no multi-threaded coding!

How can you have your cake and eat it too? Every iPhone app is a multi-threaded program, or at least its running in conjuction with the multi-threaded iPhone operating system. Use the right delegate methods in the right ways, and you can take advantage of extra threads of execution that the iPhone gives you for free without writting any multi-threaded code of your own, hence sidesteping the problem of threading bugs in your code. An iPhone app is one big event loop – your classes have methods that the event loop calls in response to stuff happening on the device and in your app. When you use the URL loading system’s asynchronous APIs, the iPhone uses a different thread than the one running your app’s event loop to load the contents of the URL, and it makes callbacks via your apps event loop when data has been downloaded.

connection = [[NSURLConnection alloc]
                          initWithRequest:request delegate:self];
 
- (void)connection:(NSURLConnection *)theConnection
                          didReceiveData:(NSData *)incrementalData

Note carefully, when data has arrived from the remote webserver, that other iPhone thread doing the downloading doesn’t make calls into your objects at the same time as your methods are running, it puts messages into your apps event loop. If it called your app directly then chances are your app would be running some UI code or something and you’d have to write thread safe code. Instead, the call that data is ready arrives as an event on the event loop. Events on the event loop run single threaded, one at a time. Using this we can get asynchrous image download from Flickr without writting thread safe code ourselves. Even better, Cocoa’s URL loading system will download those URLs in parallel! For free!

That’s all well and good, but how do you get a table view to update the UITableViewCell with the image after its already been returned? A UIImage is imutable (right?) so you can’t change its image later when the image data has downloaded. Turns out Apple made this super easy too. Instead of putting a UIImage in the UITableViewCell, you put your own UIView object, that is sized correctly for the image you want to display, into the content view of the UITableCell (as a subview). At first your view object it can be empty, or it can have a dummy image in it, or you can pop in one of those spinny ’something is happening’ views. Then when the image data is downloaded, create a UIImageView with the image and pop it in your view in the cell. Hey presto… it appears. While all this is happening the user can be scrolling and going back and forth with a fully functioning UI.

I put this all together in a class AsyncImageView, listed below. It’s use is simple

  1. alloc and initWithRect:
  2. add it to a view, eg in a table cell’s content view;
  3. send it the loadImageFromURL: message.

LoadImageFromURL will return right away, the image will load in the background, and will automatically appear in the view when its finished downloading. The code posted below is something I whipped up pretty quickly (and I didn’t leak check yet!), but hey – parallel, asynchronous image download and display in about 40 lines of code with no thread-safe worries? Works in smooth scrolling tables, even on the Edge network? I rate it a big win, and wanted to share the technique.

I’ve developed an iPhone programming training class that I will be teaching soon in the SFBay Area (though my partners and I could probably bring it to you). It’s very hands on, and specially designed to help professional programmers new to Cocoa and Objective-C over the difficult initial learning curve. In the class we build the Postcards app from start to finish, including AsyncImageView. Email me for more info: markj at markj.net

AsyncImageView.h

AsyncImageView.m

@interface AsyncImageView : UIView {
    NSURLConnection* connection;
    NSMutableData* data;
}
@end
 
@implementation AsyncImageView
 
- (void)loadImageFromURL:(NSURL*)url {
    if (connection!=nil) { [connection release]; }
    if (data!=nil) { [data release]; }
    NSURLRequest* request = [NSURLRequest requestWithURL:url
             cachePolicy:NSURLRequestUseProtocolCachePolicy
             timeoutInterval:60.0];
    connection = [[NSURLConnection alloc]
             initWithRequest:request delegate:self];
    //TODO error handling, what if connection is nil?
}
 
- (void)connection:(NSURLConnection *)theConnection
     didReceiveData:(NSData *)incrementalData {
    if (data==nil) {
          data =
          [[NSMutableData alloc] initWithCapacity:2048];
    }
    [data appendData:incrementalData];
}
 
- (void)connectionDidFinishLoading:(NSURLConnection*)theConnection {
 
    [connection release];
    connection=nil;
 
    if ([[self subviews] count]>0) {
        [[[self subviews] objectAtIndex:0] removeFromSuperview];
    }
 
    UIImageView* imageView = [[[UIImageView alloc] initWithImage:[UIImage imageWithData:data]] autorelease];
 
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.autoresizingMask = ( UIViewAutoresizingFlexibleWidth || UIViewAutoresizingFlexibleHeight );
 
    [self addSubview:imageView];
    imageView.frame = self.bounds;
    [imageView setNeedsLayout];
    [self setNeedsLayout];
    [data release];
    data=nil;
}
 
- (UIImage*) image {
    UIImageView* iv = [[self subviews] objectAtIndex:0];
    return [iv image];
}
 
- (void)dealloc {
    [connection cancel];
    [connection release];
    [data release];
    [super dealloc];
}
 
@end

And here is the usage in UITableViewCell. The AsyncImageView gets tagged with 999, and when it gets recycled, that 999 tagged view gets fished out and removed. So only the cell is being recycled, not the AsyncImageView object. When its removed from the cells content view it also gets released, causing dealloc, which in turn cancels the url download (if its outstanding).

- (UITableViewCell *)tableView:(UITableView *)tableView
       cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    static NSString *CellIdentifier = @"ImageCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
 
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
              initWithFrame:CGRectZero reuseIdentifier:CellIdentifier]
              autorelease];
    } else {
	AsyncImageView* oldImage = (AsyncImageView*)
             [cell.contentView viewWithTag:999];
	[oldImage removeFromSuperview];
    }
 
	CGRect frame;
	frame.size.width=75; frame.size.height=75;
	frame.origin.x=0; frame.origin.y=0;
	AsyncImageView* asyncImage = [[[AsyncImageView alloc]
               initWithFrame:frame] autorelease];
	asyncImage.tag = 999;
	NSURL* url = [imageDownload
               thumbnailURLAtIndex:indexPath.row];
	[asyncImage loadImageFromURL:url];
 
	[cell.contentView addSubview:asyncImage];
 
    return cell;
}

[Slashdot] [Digg] [del.icio.us] [StumbleUpon]

Comments

124 Responses to “UITables with Downloaded Images – Easy Asynchronous Code”
  1. JohnH says:

    Mark, Hi, OK I tried a couple of your suggestions. The app is behaving the same way on my phone. I didn’t try 2.2.1 yet, I’ve got some 3.0 specific code so I’ll have to create a test app to try 2.2.1. I did set a break point in the didReceiveData method, incrementalData seems fine, I hover over it and it looks like a valid object (the popup reports x number of bytes), however, as soon as I try to use it I get an “EXC_BAD_ACCESS”.

    One last oddity, the console log looks clean until I start setting break points, then I see several “unable to read unknown load command…”. They start logging right after the app starts. I ran a couple Apple sample apps and I’m seeing it there too.

    Thanks

    John

  2. JohnH says:

    Mark, I got it, stupid mistake on my part. In my cellForRowAtIndexPath I was only creating an instance of my image when the cell was equal to nil. I moved my code and it working great (Snow Leopard, SDK 3.0), thanks again for the resource. John

  3. Nik says:

    thankyou sooo much for this blog post. Taken me a while to get head around this ‘fast scrolling’ thing ;-)

    one thing that i’m still having difficulty with and its been alluded to above, that being the removing of images from dequeued cells.

    I’m using a version of the yellow jackets code above (thanks also to Wayne for the integration of URLCache, saved me a major headache).

    Can anyone give me a hand with where I need to remove the old imageview?

    if cell !=nil then { asyncImageView = (AsyncImageView *) [cell.contentView viewWithTag:ASYNC_IMAGE_TAG];
    }

    is multiple asyncImageViews the way to go?

    many thanks

    Nik

  4. Dov says:

    This is a great tutorial. I extended it to UIButton so I could click on the image. Does anyone here know how to get touches events to work with UIImageView and not UIButton?

    Happy coding,

    Dov

  5. jcoletaylor says:

    Guys, this has been a lifesaver, seriously. I’m a total iPhone newb, but this has been a really great post and set of comments. I would like to bump on Nik’s post though, I don’t seem to be able to make the previous image clear before the new one tries to load. the [oldImage removeFromSuperview] does remove it, but it removes the image and none others load back again. Either I’m confused or there’s something I’m missing…

    Thanks!

  6. smpdawg says:

    Wayne,

    Love the code, made one small tweak. I changed ine 78 in AsyncImageView.m to this

    spinny.center = CGPointMake(self.width / 2, self.height / 2);

    The original code worked well as long as the original of the AsyncImageView was 0,0. If it wasn’t, the spinner would get pushed away from the center.

  7. Dan says:

    I’ve got it working in my app, but the images are stacked on top of my cell textLabels. Anyone know of an easy remedy for this?

  8. Ed Park says:

    Brilliant. I’m currently porting earthalbum to the iPhone. I don’t usually comment on these types of threads, but this is brilliantly clear and useful code– thanks a ton.

  9. Bjoern says:

    Hey Guys.

    Thank you for this amazing post. No more scrolling problems. I was hanging around with this problem for a while… Thanks a lot!!!!!!!!!!

  10. The says:

    Hi!
    Can someone please make a sourefile of this great tutorial?
    I can’t get it work. Thanks!

  11. Kiran says:

    Hi Mark,

    Great Article.. but I have one problem, images are not retained when I scroll down and then back up. It always reloads the images while scrolling. Is it possible to retain the images one they are loaded initially. Thanks in advance.

  12. Johnny says:

    Hey, Umm… I’m trying to implement this in my application… having some problems… not seeing the image in my rss feeder / wall application.

    I bring in the URL from the in an rss feed i setup.

    Here is my table cell thingie… I’m seeing the full url in the NSLog i setup … so I know I’m grabbing that just fine… any comments?

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *MyIdentifier = @”MyIdentifier”;
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];

    if (cell == nil) {
    cell = [[[UITableViewCell alloc]
    initWithFrame:CGRectZero reuseIdentifier:MyIdentifier]
    autorelease];
    } else {
    AsyncImageView* oldImage = (AsyncImageView*)
    [cell.contentView viewWithTag:999];
    [oldImage removeFromSuperview];
    }

    // Set up the cell
    int postIndex = [indexPath indexAtPosition: [indexPath length] – 1];
    cell.textLabel.text = [[posts objectAtIndex: postIndex] objectForKey: @”title”];

    NSString *urlString = [[posts objectAtIndex: postIndex] objectForKey: @”link”];
    NSURL *url = [NSURL URLWithString:urlString];

    NSLog(@”%@”, urlString);

    CGRect frame;
    frame.size.width=75; frame.size.height=75;
    frame.origin.x=0; frame.origin.y=0;

    AsyncImageView* asyncImage = [[[AsyncImageView alloc] initWithFrame:frame] autorelease];
    asyncImage.tag = 999;

    [asyncImage loadImageFromURL: url];

    [cell.contentView addSubview:asyncImage];

    /*UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0,0,45,45)]; // set the location and size of your imageview here.
    imageView.image = [UIImage imageNamed:@"mzwLogo.gif"]; // set the image for the imageview
    //imageView.image = loadImage.image;
    [cell addSubview:imageView];*/

    NSLog(@”just set up the cell”);

    return cell;
    }

  13. Zeeshan Khan says:

    Nice post, it removes the bug of scrolling table content.
    Thanks

  14. jrgresh says:

    What happened to Wayne’s post and codel? I see people mention it but I don’t see the code. I’d love to add caching to this. And thanks for the async image loading, a big help to me too.

  15. Zeeshan Khan says:

    i got a problem with this solution, after scrolling down images gets disappear and again it loads image.
    is there any solution for that..?
    Thanks in advance

  16. Zeeshan Khan says:

    how can we retain those images which are loaded initially ?

  17. arielm says:

    Thanks for this useful and inspiring post!

    I think I found a leak (fair enough as you explicitly warned about the lack of leak-check…)

    NSURLRequest* request = [NSURLRequest requestWithURL:url ... timeoutInterval:60.0];
    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];

    At this point, you should do a [request release].
    Or alternatively, you could use: [NSURLRequest initWithURL: url ... timeoutInterval:60.0];

    Does it make sense?

  18. Tim says:

    Hi Mark,

    So many thanks for this class. it’s a true life saver. one small problem i’m having.
    if somebody could email timfazio@gmail.com if they know what’s going on, that would save many late nights.

    I have an app which searches for cafes and displays them. I am using the async image view class with the

    now, the images display the loading spinny icon until i scroll to the bottom of the table and back to the top. only after scrolling back to the top does the image load. i can’t work out how to make them display as soon as the table is loaded, like they used to? all the references are correct, as the images do evenutally load after you’ve scrolled to the bottom of the table.

    i’m wondering if it has something to do with dequeing and reusing cells or something, but i just can’t work it out– the class works fine in another table, the only difference being the other table is loaded from code not a nib. that said, it worked fine when it was loaded from a nib…

    here’s the code reference for the table that’s not working

    Code:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CustomCellIdentifier = @”CustomCellIdentifier “;

    CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier];

    AsyncImageView *asyncImageView = nil;

    if (cell == nil) {
    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@”CustomCell” owner:self options:nil];
    for (id oneObject in nib)
    if ([oneObject isKindOfClass:[CustomCell class]])
    cell = (CustomCell *)oneObject;

    CGRect asynImageRect = CGRectMake(10, 10, 60, 60);
    asyncImageView = [[AsyncImageView alloc] initWithFrame:asynImageRect];
    asyncImageView.tag = 9000;
    [cell.contentView addSubview:asyncImageView];
    [asyncImageView release];

    }
    Cafe *bCafe = [appDelegate.cafes objectAtIndex:indexPath.row];
    cell.txtMain.text = bCafe.cafename;

    …. load other variables here ….

    if (!asyncImageView)
    asyncImageView = (AsyncImageView*)[cell viewWithTag:9000];

    NSString *cafeImage = bCafe.image;
    cafeImage = [cafeImage stringByReplacingOccurrencesOfString:@"." withString:@".cropped1."];
    NSString *cafeImageString = [NSString stringWithFormat:@"http://www.mysite.com/images/%@", cafeImage];
    NSURL *cafeImageStringURL = [NSURL URLWithString:cafeImageString];

    [asyncImageView loadImageFromURL:cafeImageStringURL];

    return cell;
    }

    and for the working table…

    Code:

    -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    NSLog(@”TABLING”);
    static NSString *CellTableIdentifier = @”CellTableIdentifier “;

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
    CellTableIdentifier];

    AsyncImageView *asyncImageView = nil;

    if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier: CellTableIdentifier] autorelease];

    //set avatar image
    CGRect asynImageRect = CGRectMake(20, 20, 60, 60);
    asyncImageView = [[AsyncImageView alloc] initWithFrame:asynImageRect];
    asyncImageView.tag = 9000;
    [cell.contentView addSubview:asyncImageView];
    [asyncImageView release];

    … set up other variables here …
    }

    int currentCell = indexPath.row + rowAdder;

    …. load other variables here ….

    //fetch user avatar from web
    if (!asyncImageView)
    asyncImageView = (AsyncImageView*)[cell viewWithTag:9000];
    Review *gReview = [appDelegate.reviews objectAtIndex:currentCell];
    NSString *avatarImage = gReview.avatar;
    avatarImage = [avatarImage stringByReplacingOccurrencesOfString:@"." withString:@".cropped1."];

    if ([avatarImage isEqualToString:@"empty"]) {
    NSString *placeholderString = @”http://www.mysite.com/images/avatar.jpg”;
    NSURL *placeholderUrl = [NSURL URLWithString:placeholderString];
    [asyncImageView loadImageFromURL:placeholderUrl];
    }
    else {
    NSString *avatarImageString = [NSString stringWithFormat:@"http://www.mysite.com/avatars/%@", avatarImage];
    NSURL *avatarImageStringURL = [NSURL URLWithString:avatarImageString];
    [asyncImageView loadImageFromURL:avatarImageStringURL];
    }

    return cell;
    }

    i hope i’m blind and have just forgotten something very simple…
    any ideas? it’s driving me nuts!

  19. swathi says:

    When loading images i am not getting full image for the first time i.e., i am getting image with some gray color. Why this happens. when i log, i am getting Corrupt JPEG data: premature end of data segment.

    Can anyone help me out…………..

    Thanks and Regards,
    Swathi M

  20. Kevin says:

    Hi!! Many people are looking for the same improvement: caching images already loaded for cells already scrolled. Scrolling up and down causes the reloading of previous loaded images. Someone talks about Wayne improvement which solves this issue, but it’s no more on this post.

    Please help us!
    Thanks in advance ;)

  21. Thanos says:

    Anyone has any ideas on how to extend this to set the image in an UIButton?

  22. Samy says:

    Has anyone been able to add a spinner in the aboce AsynImageView class, I am struggling with this.
    For info, I al using the async donwload for loading two picture in my code the following way:

    CGRect frame;
    frame.size.width=300; frame.size.height=100;
    frame.origin.x=0; frame.origin.y=0;

    AsyncImageView* asyncImage = [[[AsyncImageView alloc] initWithFrame:frame] autorelease];
    NSURL *url_img = [NSURL URLWithString:newencodedImgUrl];
    [asyncImage loadImageFromURL:url_img];
    [imgGraphView addSubview:asyncImage];

    AsyncImageView* asyncImage2 = [[[AsyncImageView alloc] initWithFrame:frame] autorelease];
    NSURL *url_img2 = [NSURL URLWithString:encodedImgUrl];
    [asyncImage2 loadImageFromURL:url_img2];

    [pieGraphView addSubview:asyncImage2];

    An I have tried to add the spinner in AsynImageView like this:
    In loadImageFromUrl:

    piespinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
    piespinner.center = CGPointMake(self.center.x, self.center.y);
    [self addSubview:piespinner];
    [self bringSubviewToFront:piespinner];

    And in the connnectiondidfinishLoading:

    [piespinner stopAnimating];
    [piespinner release];

    If anyone can help that’ll be much appreciated

  23. Samy says:

    Sorry for the spam, finally got it working. The code if anyone is interrested in loadImageFromUrl

    piespinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
    piespinner.center = CGPointMake(self.center.x, self.center.y);

    [self addSubview:piespinner];
    [piespinner startAnimating];

  24. Nerdhappy says:

    MarkJ, great, great script, thanks a million.

    Regarding the caching images problem, we solved that using this:

    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:nil] autorelease];

    Good Luck!

  25. James says:

    Hello There this has been a great resource for me I want to thank mark a lot. For the viewers that want to know where wayne’s post is click on the link to older comments it is below the last comment on the page right before the word Trackbacks. I have implemented the yellowjacket code into my app.The problem is my app loads a lot of images in like over 1,000. Therefore allocating a lot of memory.

    My question is for those of you using the yellojacket code. I would like to know how to empty the cache of the image objects. Within my view did disappear method of the table view’s delegate. Or in my app delegate’s did receive memory warning method.

    The implementation of this would be greatly appreciated as I have slamming my head against the wall for about three days know trying to do this. I know it is something small that i’m just not doing properly. Thank you

  26. Rob Gibbens says:

    Excellent tutorial. Thank you so much. I tried a bunch of different ways to get this to work before finding this site.

    I’ve ported the code from Objective-C to MonoTouch (C#). Would you mind if I blogged about it, giving full reference to this site of course?

  27. James says:

    Ok people i finally found the way to empty the cache from the yellow jackets code. This code works for me. Hope it helps you out.

    Make sure to make the reference to these methods in the header files of the class specified.

    First you will need to add a method to the ImageCache Class to empty the NSDictionary in that class something like this.

    -(void)emptyCache{

    [myDictionary removeAllObjects];

    NSLog(@”all objects removed Cache has been empty”);
    }

    Second You will add an identical method in the AsyncImageView class.
    like this

    -(void)emptyImages{

    [imageCache emptyCache]; // tells the image class to implement the empty cache method
    }

    And lastly in your TableViewController Class. in the dealloc method add this line of code between the brackets.

    [asyncImage emptyImages];

    I implement this when my table view gets deallocated although you might want to els ware

    I hope this helps the code has not been fully tested for leaks but if I am missing something feel free to modify.

  28. Robert A. says:

    Great Post. Thanks to everyone that contributed. I’m trying out the YellowJacket code and added what I hope is an improvement that solves the caching issue that a few users mentioned with earlier images showing up before new ones are loaded.

    In AsyncImageView.m, line 71, I’ve provided an alternative image if a cached image does not exist for the current cell.

    //————existing code—————

    if (imageCache == nil) // lazily create image cache
    imageCache = [[ImageCache alloc] initWithMaxSize:2*1024*1024]; // 2 MB Image cache

    [urlString release];
    urlString = [[url absoluteString] copy];
    UIImage *cachedImage = [imageCache imageForKey:urlString];
    if (cachedImage != nil) {
    if ([[self subviews] count] > 0) {
    [[[self subviews] objectAtIndex:0] removeFromSuperview];
    }
    UIImageView *imageView = [[[UIImageView alloc] initWithImage:cachedImage] autorelease];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.autoresizingMask =
    UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self addSubview:imageView];
    imageView.frame = self.bounds;
    [imageView setNeedsLayout]; // is this necessary if superview gets setNeedsLayout?
    [self setNeedsLayout];
    return;
    //———– Line 71 ————

    } else {
    // Use a default placeholder when no cached image is found
    UIImageView *imageView = [[[UIImageView alloc] initWithImage:[UIImage
    imageNamed:@"placeholder.png"]] autorelease];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.autoresizingMask =
    UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self addSubview:imageView];
    imageView.frame = self.bounds;
    [imageView setNeedsLayout];
    [self setNeedsLayout];
    }

    Be sure not to include a “return;” at the end of this else block or all your thumbs will be your default placeholder. You want the code to continue to run after you insert the placeholder.

    Let me know if you find any problems!

  29. kan says:

    hi,
    i try to test on the loadImageFromURL, and don’t know how to get the image which should be return.

    should you write out the usage of it in detail ?

    Thank you for your help

    Kan

  30. Wolf says:

    Hi there.

    Now this is truly a good piece of code. ANYWAY, HERE IS A TRICKY ONE: Can anyone figure out how to get device orientation change to work? If I setup something like that :

    CGRect frame;
    frame.size.width=320; frame.size.height=480;
    frame.origin.x=0; frame.origin.y=0;
    AsyncImageView* asyncImage = [[[AsyncImageView alloc] initWithFrame:frame] autorelease];

    and I am in portrait mode an image which is larger than 320×480 will be autoresizedtofit. good so far. but then when I turn the device into landscape mode the image stays in size (does not resize itself to autowidth) and it sits at the very left. its like the whole frame with its 320×480 is rotated and does not resize itself to the landscape mode.

    Anybody a solution to that?

  31. sabes says:

    Is anyone having crashes of the code with iPhone SDK 3.2 Beta?

  32. radha says:

    Hi !
    I have been trying to sort the cells in my UITable view in alphabetical order.
    can anybody help me.

    thanx in advance.

  33. Flo says:

    Thanks for the code. It really works great so far. But I am facing one pretty odd problem here. In my app the images do not start showing up before I scroll my table down a few lines. It seems to be similar to Tim’s problem described above. Could this be related to the fact that I use NIB files for my custom table cell? Any ideas? Need code? Thanks for your help!

  34. Dave says:

    Hey,

    This code is great, but I have one problem. When the table initially loads, the images do not (though my text does). Once I scroll back and forth, they do begin loading. I guess I am missing something on the initial load, but I am not sure what.

    Anyone else having this issue?

    Thanks again for the code. It is great.

  35. HBR says:

    A big big thanks for this code. I am making my first steps in iphone dev and this code really saved me a big time.
    I have a little problem though, when I pass a constant to loadImageFromUrl, it works great, but when I use a variable it is passed as nil.

    Here is my cellForRowAtIndexPath :

    // Customize the appearance of table view cells.
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @”TnsContentCell”;

    Content *currentItem = [articles objectAtIndex:indexPath.row];
    NSString *cellTitle = [currentItem title];
    NSString *date = [currentItem date];
    NSString *category = [currentItem category];
    NSString *teaser = [currentItem teaser];
    NSString *thumbnailURL = [currentItem thumbnailURL];

    UILabel *mainLabel, *detailLabel, *dateLabel;
    UIActivityIndicatorView *activity;

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
    // Normal loading
    //cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
    //cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;

    // Custom cell in nib file
    [[NSBundle mainBundle] loadNibNamed:@”ContentCell” owner:self options:nil];

    cell = contentCell;
    self.contentCell = nil;
    }
    else {
    AsyncImageView* oldImage = (AsyncImageView*) [cell.contentView viewWithTag:999];
    [oldImage removeFromSuperview];
    }

    CGRect frame;
    frame.size.width=60;
    frame.size.height=60;
    frame.origin.x= 10;
    frame.origin.y= 30;

    AsyncImageView* asyncImage = [[[AsyncImageView alloc] initWithFrame:frame] autorelease];
    asyncImage.tag = 999;

    //thumbnailURL = @”http://www.tuniscope.com/uploads/images/content/coca-250310-v.jpg”;
    NSURL * url = [NSURL URLWithString:thumbnailURL];
    [asyncImage loadImageFromURL:url];

    [cell.contentView addSubview:asyncImage];

    mainLabel = (UILabel*)[cell.contentView viewWithTag:1];
    activity = (UIActivityIndicatorView*)[cell.contentView viewWithTag:2];
    dateLabel = (UILabel*)[cell.contentView viewWithTag:4];
    detailLabel = (UILabel*)[cell.contentView viewWithTag:5];

    mainLabel.text = cellTitle;
    dateLabel.text = date;
    detailLabel.text = [[NSString alloc] initWithFormat:@”%@ – %@”, [category uppercaseString], teaser];
    [dateLabel setHidden:YES];
    return cell;
    }

    This code won’t work, although I’m more than certain the thumbnailURL has a valid url string in it (I alerted it).
    If I use a constant string it, it will work fine … here it is :

    //thumbnailURL = @”http://www.tuniscope.com/uploads/images/content/coca-250310-v.jpg”;
    NSURL * url = [NSURL URLWithString:thumbnailURL];
    [asyncImage loadImageFromURL:url];

    Any idea why this is happening please ?

  36. Mat says:

    Hi, thanks for this trick!!!….
    Just a question, in witch way can i keep the image on the cell??
    if i scroll down the table i lost information about the cells (the images too) that are no more displayed, and when scroll down again, the image is re-downloaded.

    TIA
    Sorry for my bad english!

  37. Terry says:

    I love this code! But I’m pulling my hair out over something that should be easy.

    I want to have a picture displayed during the load process (along with a spinner), and if the load fails, then a different picture displayed to the user. I can get the spinner, and the “failed” image to display just as needed, but for some bizarre reason the “loading” image will not display? The code is nearly identical, and I’ve stepped through it several times to ensure the images are pulled from disk correctly, and no obvious error occurs. Argh!

    I have tried different images, in case that was the problem. I have tried using different approaches to load the image, in case that was the problem. I have tried so many things! ANY suggestions would be appreciated

    (I have copied & pasted the code below, but flattened my call structure to be simpler to show. This may have introduced some errors but it does all compile and run in the original form)

    I have the “failed to load” part as coded below:

    - (void)connection:(NSURLConnection *)theConnection didFailWithError:(NSError *)error
    {
    UIImage *anImg = [[UIImage imageNamed:@"load_failed.png" ]retain];

    UIView *prevImg = [self viewWithTag:ASYNC_PHOTO_TAG];
    [prevImg removeFromSuperview];

    //make an image view for the image
    UIImageView* imageView = [[[UIImageView alloc] initWithImage:anImg] autorelease];

    //make sizing choices based on your needs, experiment with these. maybe not all the calls below are needed.
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.autoresizingMask = ( UIViewAutoresizingFlexibleWidth || UIViewAutoresizingFlexibleHeight );
    imageView.tag = ASYNC_PHOTO_TAG;
    [self addSubview:imageView];
    imageView.frame = self.bounds;
    [imageView setNeedsLayout];
    [self setNeedsLayout];

    //clean up
    [connection release]; connection=nil;
    [data release]; data=nil;
    }

    However, almost identical code executed BEFORE the load begins fails to display any picture. It DOES display the spinner though (!?)

    - (void)loadImageFromURL:(NSURL*)url placeHolderImg:(UIImage *)placeholderImg;
    {

    UIView *prevImg = [self viewWithTag:ASYNC_PHOTO_TAG];
    [prevImg removeFromSuperview];

    //make an image view for the image
    UIImageView* imageView = [[[UIImageView alloc] initWithImage: placeholderImg] autorelease];

    //make sizing choices based on your needs, experiment with these. maybe not all the calls below are needed.
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.autoresizingMask = ( UIViewAutoresizingFlexibleWidth || UIViewAutoresizingFlexibleHeight );
    imageView.tag = ASYNC_PHOTO_TAG;
    [self addSubview:imageView];
    imageView.frame = self.bounds;
    [imageView setNeedsLayout];
    [self setNeedsLayout];

    UIActivityIndicatorView *spinny = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
    spinny.tag = SPINNY_TAG;
    spinny.center = CGPointMake(120, 100); // self.center;
    [spinny startAnimating];
    [self addSubview:spinny];
    [spinny release];

    [self loadImageFromURL:url];
    }

    – and in case anyone else wants to handle load failures gracefully, you should also implement this to detect errors from the server. You would think this would result in calls to loadDidFail, but it doesn’t!

    - (void)connection:(NSURLConnection *)theConnection didReceiveResponse:(NSURLResponse *)response {
    if ([response respondsToSelector:@selector(statusCode)])
    {
    int statusCode = [((NSHTTPURLResponse *)response) statusCode];

    switch (statusCode)
    {
    case 403: // forbidden
    case 404: // not found
    case 500: // (server) internal error
    case 501: // (server) not implemented
    case 502: // (server) overloaded

    // end processing!
    [connection cancel];
    [self picCantBeLoaded];

    //clean up
    [connection release]; connection=nil;
    [data release]; data=nil;
    break;

    default:
    // do nothing.
    break;
    }
    }
    }

  38. Terry says:

    Ok, made some progress. It has something to do with the frame / center of the image. Phew!

    imageView.frame = self.bounds;

    Is the problematic line. Still working on understanding why, and what it should be.

  39. Antonio says:

    @HBR
    Make your urlstring a valit NSURL using stringByAddingPercentEscapesUsingEncoding

    NSURL* url = [NSURL URLWithString: [thumbnailURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] ];

  40. CM says:

    Thank you for the wonderful code, this is the first time i have just pasted something into my project and it ran the first time. Thank you!

  41. Mario says:

    Yeeeeeeeessssssssss!!!!

    thanks!!!

  42. Richard says:

    Fantastic Code,

    but as always i need some help :)

    i got everything working fine , but if i want to thread the downloading of my data for the tableviews , and display a loading screen in the process, all of a sudden the image views in my first cell wont load until i push them off screen and on again, and i have no idea how to fix it.

    if(tableView == newsTable){
    EnglandNewsCustomCell *cell = (EnglandNewsCustomCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
    [[NSBundle mainBundle]loadNibNamed:@”EnglandNewsCustomCell” owner:self options:nil];
    cell = self.englandNewsCustomCell;

    } else {
    AsyncImageView* oldImage = (AsyncImageView*)
    [cell.contentView viewWithTag:999];
    [oldImage removeFromSuperview];

    }
    EnglandStory *engStory = [englandStories objectAtIndex:indexPath.row];
    CGRect frame;
    frame.size.width=67; frame.size.height=45;
    frame.origin.x=10; frame.origin.y=8;

    AsyncImageView* asyncImage = [[[AsyncImageView alloc]
    initWithFrame:frame] autorelease];

    asyncImage.tag = 999;

    NSURL *url = [NSURL URLWithString:engStory.picture];
    [asyncImage loadImageFromURL:url];

    //load data into cell of news table
    if(tableView == newsTable){

    EnglandStory *engStory = [englandStories objectAtIndex:indexPath.row];
    cell.headline.text = engStory.name;
    cell.sub.text = engStory.intro;

    [cell.contentView addSubview:asyncImage];
    return cell;

    this is my code for my cell for row at index path, which i presume is the problem , i just cant find a fix, i know there must be one out there because apps like the sky news on manage it , help me please

  43. Brian L says:

    This was my solution for multiple images showing up in one AsyncImageView. This was happening in a custom table view cell.

    In AsyncImageView.m, line 71, I add the following block to remove the view.

    else {
    if ([[self subviews] count] > 0) {
    [[[self subviews] objectAtIndex:0] removeFromSuperview];
    }
    }

  44. Chris says:

    Hi

    Great tutorial thanks.

    One question though. I am using a custom cell nib which has a ImageView and an IBOutlet… How can I fill that imageview?

  45. That’s indeed a great idea :) You are damn right … actually connection:didReceiveData: is doing exactly what I have in my custom remote image loading operation class …

    If I have to be pedantic (don’t really but just to mention) the urlrequest multithreading only works in the case when all the images being loaded have the same priority :) With operations you can also set priority and give certain images more priority over others …

    However, great post, thanks for sharing !

  46. Yann says:

    Thank you SO MUCH !

    That’s bang on what I needed.
    Cheers

  47. Matt says:

    If I understand correctly. This article explains how to have a smooth scrolling table but images do not actually download while mid-scroll (while scrolling). The requests will be created but the NSURLConnection delegate methods will not actually be called till scrolling has finished or otherwise stopped.

    right?

    Thanks,

    Matt

  48. markj says:

    Right. Loading durring scrolling needs more sophisticated code, which can work well on 2nd & 3rd generation devices (faster CPU and graphics), but makes scrolling ‘lumpy’ on original iPhones.

  49. Patrick says:

    Ok, so I see the question that people seem to be having over and over again, about images being removed after the scroll, here’s my solution:

    this method

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    after the else cell!=nill)

    change to:

    in the AsyncImageView* oldImage = (AsyncImageView*)
    [trendCell.contentView viewWithTag:999];
    //[oldImage removeFromSuperview];
    AsyncImageView* oldImage = (AsyncImageView*)
    [trendCell.contentView viewWithTag:999];
    //[oldImage removeFromSuperview];

    why do we want to remove the image if the scrolling has moved anyway? N00b here, so i dunno?

  50. I love the idea, but having trouble following what I should add from comments. Does anyone have a completed, updated class that they can make available? Is it located somewhere on github or google code and I’m just not finding it? Thanks!

Trackbacks

Check out what others are saying about this post...


Speak Your Mind

Tell us what you're thinking...
and oh, if you want a pic to show with your comment, go get a gravatar!

  • Categories

  • markjnet