PDA

View Full Version : Loading remote images for UITableViewCell




Luke Redpath
Aug 30, 2008, 01:01 PM
I'm struggling to come up with an efficient and performant way of loading remote images (over a local wifi network) to be used as images in a UITableViewCell.

What I'm basically trying to achieve is something similar to what the Apple iTunes Remote app does - when you browse a list of albums, it retrieves the artwork from the machine running iTunes and displays it on the left. The Remote app obviously does this asynchronously as the artwork doesn't appear immediately the first time you view a row.

Here's what I have so far: a custom UITableViewCell subclass that represents an Album; it has two properties, album name and album artwork URL, and two views, a UILabel and a UIImageView. When the album name is set, the UILabel is updated. This works just fine. When the artwork URL is set, the idea is that an asynchronous HTTP connection downloads the artwork an dupdates the UIImageView when its finished.

Here is my code for a custom AsyncArtworkFetcher class which, given a URL, downloads the artwork asynchronously and invokes a method on a delegate object when finished:


@implementation AsyncArtworkFetcher

@synthesize delegate;
@synthesize url;
@synthesize userData;

- (void)fetch;
{
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if(connection) {
receivedData = [[NSMutableData data] retain];
}
}

#pragma mark NSURLConnection Delegate Methods

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
UIImage *downloadedImage = [UIImage imageWithData:receivedData];
SEL delegateSelector = @selector(artworkFetcher:didFinish:);
if([delegate respondsToSelector:delegateSelector]) {
[delegate performSelector:delegateSelector withObject:self withObject:downloadedImage];
}
[receivedData release];
[connection release];
}

@end


And here's the relevant part of my custom table view cell class:


- (void)setArtworkURL:(NSURL *)newURL;
{
if(newURL != artworkURL) {
[artworkURL release];
artworkURL = [newURL retain];
AsyncArtworkFetcher *artworkFetcher = [[AsyncArtworkFetcher alloc] init];
artworkFetcher.url = artworkURL;
artworkFetcher.delegate = self;
[artworkFetcher fetch];
}
}

#pragma mark AsyncArtworkFetcher Delegate Methods

- (void)artworkFetcher:(AsyncArtworkFetcher *)fetcher didFinish:(UIImage *)artworkImage;
{
albumArtView.image = artworkImage;
[self setNeedsDisplay];
}


The main issue with this approach is down to the way in which UITableViewCell objects are reused. As you scroll through the list, when a cell is reused it will display the artwork for another album until its updated (its only briefly but it just looks wrong). One approach would be to have a unique cell for each album but this defeats the whole point of reusable cells.

Apart from this one issue, this works well enough in the simulator, but running on my actual iPhone it just locks the phone up, probably because of the number of asynchronous requests it's firing off at once. I guess I need to cache the downloaded image in some way (although I thought NSURLConnection dealt with caching automatically - perhaps I'm not configuring it correctly).

Any suggestions on a better way of going about this? Maybe I'm missing a really obvious technique - it must be possible, the Apple Remote app shows it can be done without any serious performance penalties.



matrix4123
Sep 1, 2008, 05:08 PM
I am having a similar issue also. I am currently just fetching the image when the table view cell comes into view and then caching the image in a dictionary by URL key... Please let me know if you have figured out a better way to do this. THANKS!

Garrett
Sep 1, 2008, 10:40 PM
Just use [UIImage initWithData:[NSData dataFromContentsOfURL:[NSURL URLWithString:@"URLHERE"]]];

Luke Redpath
Sep 2, 2008, 04:14 AM
Garret - not sure if you even read my post. I know how to load a remote image. Doing that in a synchronous (blocking) way is certainly not going to help with my performance issues.

robbieduncan
Sep 2, 2008, 04:47 AM
I'd create a queue system. Instead of creating an instance of AsyncArtworkFetcher I'd make this a singleton object (this example (http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/chapter_3_section_10.html) shows the Apple suggested method of creating a singleton). I'd then change the way this works to enable the queuing of an image to be downloaded along with a target and action to call when the image downloads, probably along with some sort of tag to let the called object know which image was downloaded.

Some the interface to AsyncArtworkFetcher would look something like


@interface AsyncArtworkFetcher
{
}

- (AsyncArtworkFetcher *) sharedAsyncArtworkFetcher;
- (void) queueImage:(NSURL *) imagePath withIdentifier:(id) identifier andCallbackTarget:(id) target andCallbackMethod:(SEL) method;


When queue image is called push a dictionary (or a specific data storage class if you want to write one) with the 4 parameters into the queue (an NSMutableArray). If there is not a download in progress start one.

The download method should download as per you existing code using the request at the start of the queue. When complete perform method on target with the parameter identifier to inform the original caller the download is complete (and I suppose pass them the image). If the queue is not empty move onto the next item in the queue.

This should limit the amount of NSURLConnections in flight to one and hopefully prevent lockups. If this is too slow try increasing the number of connections you have in flight at any time...

Edit to add: and I agree blocking is not a good idea. It would most likely freeze the UI totally which is not going to be acceptable to the users. You might need to enhance the above idea to enable the cancelling of requests if it's possible for the view to disappear before a download completes.

Further edit to add: adding caching to this singleton approach is easy: simply use a NSMutableDictionary. When you download an image push it into the dictionary using the URL as the key. When get a request for an image just check the cache first. Of course you will need to ensure that when the OS lets know you are running out of memory you flush the cache...

Luke Redpath
Sep 2, 2008, 10:07 AM
This sounds like it might work although I'm wondering if I could (should?) create an NSOperation and use NSOperationQueue rather than rolling my own.

Funnily enough, I stumbled upon this approach yesterday as a good way of pushing my long running data requests into the background.

I had originally faced the prospect of switching my existing JSONRPC class over to use the aysynchronous API but the biggest issue with that approach was having to bubble up my delegate messages through the various layers of my app (the JSONRPC is at the lowest level and has a couple of domain-specific layers on top of it before it gets to the controller).

NSOperation has worked a treat with that so maybe it will work in this case too. Will update on how it goes later.

robbieduncan
Sep 2, 2008, 10:45 AM
This sounds like it might work although I'm wondering if I could (should?) create an NSOperation and use NSOperationQueue rather than rolling my own.

Yeah, that's probably a good idea. I've never used them as until the iPhone came along I couldn't target a platform where they were guaranteed to exist due to old versions of Mac OSX not having them.

PhoneyDeveloper
Sep 3, 2008, 12:19 AM
First, you need a proper MVC design. You need a separate data model object that holds the album art and name and you need an array of these objects. This model object will own the art fetcher object.

Your cell is only for display. It just displays text and images and knows nothing about urls and remote anything. When the url is finished loading the model object will notify the view, indirectly, that the view should reload the image from the model. You can do this notification either via the UIViewController, or by an NSNotification, or maybe by KVO. Simplest, probably is have the model object call the UIVIewController and then the view controller will know which cell needs to be notified and then it calls the cell.

The model objects will be initialized either with an image that is nil or with a placeholder image (a question mark or something).

You may need to throttle the number of connections to say 5 or 6. You could use NSOperation for that. I'm pretty sure it can be configured to only run a maximum number of Operations.

Luke Redpath
Sep 5, 2008, 06:25 AM
Thanks for the heads up on queuing - the NSOperationQueue was a step in the right direction and I eventually got it working perfectly.

I'd quite like to write a blog post about this as I think it's a useful technique, not sure if I can with the NDA though. :(

robbieduncan
Sep 5, 2008, 06:58 AM
Thanks for the heads up on queuing - the NSOperationQueue was a step in the right direction and I eventually got it working perfectly.

I'd quite like to write a blog post about this as I think it's a useful technique, not sure if I can with the NDA though. :(

Well NSOperationQueue, NSOperation, NSURLRequest and NSURLConnection are all available in Leopard and can be talked about freely. As long as you don't include any iPhone specific information I don't see that there would be a problem...

Luke Redpath
Sep 5, 2008, 08:02 AM
Unfortunately the post I have in mind is quite iPhone-specific, as it relates to the way UITableViewCell works. Oh well, I shall have to have a think about it. It doesn't seem to have stopped other people posting iPhone dev tutorials.

robbieduncan
Sep 5, 2008, 08:04 AM
Unfortunately the post I have in mind is quite iPhone-specific, as it relates to the way UITableViewCell works. Oh well, I shall have to have a think about it. It doesn't seem to have stopped other people posting iPhone dev tutorials.

Ah. I assumed you could post something more generic about using the operation structure to manage a number of connection objects. I'd personally not make blog posts that are clearly breaking the NDA.

sebest
Sep 9, 2008, 06:01 PM
Unfortunately the post I have in mind is quite iPhone-specific, as it relates to the way UITableViewCell works. Oh well, I shall have to have a think about it. It doesn't seem to have stopped other people posting iPhone dev tutorials.

Hi Luke

Do you plan to blog about this technic, i would be really interested in reading about it!

Thanx

Luke Redpath
Sep 9, 2008, 06:03 PM
Haven't decided yet to be honest...I'm thinking about sending an email to the guys who run the O'Reilly iphone blog and see if they've had any trouble.

sebest
Sep 9, 2008, 06:13 PM
Haven't decided yet to be honest...I'm thinking about sending an email to the guys who run the O'Reilly iphone blog and see if they've had any trouble.

I see :(

And would accept to send the specific code by private message?

I would understand if you are not ok with it :)

matrix4123
Sep 15, 2008, 08:31 AM
I would be interested in learning how you did this solution as well. Could you private message me?

bocarones
Oct 1, 2008, 03:38 PM
I have ready the above answers a couple of times, two things remain unclear to me:
- how do you get around the fact that TableViewCell objects are reused for every row in a table? I would assume that creating a new instance for every row is not a good idea, but how to make sure the right row is updated once the image becomes available?
- can someone post an example of using the NSOperation code?

Thanks alot in advance!

youPhone
Oct 6, 2008, 04:05 PM
I made an image loader class, and I ran into a problem where using URLs for the keys of my NSMutableDictionary was causing multiple entries for the same key. I think it was because the URLs were too long. If you're experiencing this problem, then you need to hash the URLs and you'll get a nice integer that's suitable as a dictionary key:

+ (NSString*) urlToHashString:(NSURL*)aURL
{
return [NSString stringWithFormat:@"%U",[[aURL absoluteString] hash]];
}

- (NSData*) dataForURL:(NSURL*)aURL
{
return [imageCache valueForKey:[ImageLoader urlToHashString:aURL]];
}

- (void) addImageDataToCache:(NSData*)aDatum forURL:(NSURL*)aURL
{
[imageCache setValue:aDatum forKey:[ImageLoader urlToHashString:aURL]];
}

youPhone
Oct 6, 2008, 04:34 PM
- how do you get around the fact that TableViewCell objects are reused for every row in a table? I would assume that creating a new instance for every row is not a good idea, but how to make sure the right row is updated once the image becomes available?


You can find some code for the NSOperation here
http://forums.macrumors.com/showthread.php?t=571887&highlight=nsoperation

That will give you an example of doing an NSOperation


Here's the flow I have. I have an ImageLoader class that's based on NSObject. It's set up like a "regular" class except that I have a couple of static variables defined in the .m file:


static NSOperationQueue *imageLoadingQueue;
static NSMutableDictionary *imageCache;


This allows you to access the same NSOperationQueue and imageCache for all instances of ImageLoader.

I also have a custom NSOperation created like the one in the link I gave (except only using the "main" method like I state at the bottom of that thread that I linked).

So the ImageLoader class creates a new NSOperation which will download the data and send that data back to the ImageLoader class through a callback function.

The ImageLoader callback function then makes an Image out of that NSData and sends the image back to the UITableCell that called it which then sets the image to the correct UIImageView


So here's some example snippets of code:

In the UITableCell class, it calls:


[imageLoader loadImageFromURL:feedData.avatarURL withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];


The loadImageFromURL:withCallbackTarget:withCallbackSelector: method in ImageLoader will create a new DataLoaderOperation and put it in the queue:
DataLoaderOperation *op = [DataLoaderOperation queueDataLoadWithURL:anImageURL withIdentifier:identifierCounter withCallbackTarget:self withCallbackSelector:@selector(sendImageBack:)];
[imageLoadingQueue addOperation:op];

Then the main method in DataLoaderOperation class looks like:


@implementation DataLoaderOperation

@synthesize callbackTarget;
@synthesize callbackSelector;
@synthesize identifier;
@synthesize imageURL;

+ (id) queueDataLoadWithURL:(NSURL*)anImageURL withIdentifier:(NSInteger)anIdentifier withCallbackTarget:(id)aTarget withCallbackSelector:(SEL)aSelector
{
NSLog(@"Made an NSOperation withId:(%d)", anIdentifier);
DataLoaderOperation *thisDataLoaderOperation = [[[DataLoaderOperation alloc] init] autorelease];

thisDataLoaderOperation.callbackTarget = aTarget;
thisDataLoaderOperation.callbackSelector = aSelector;
thisDataLoaderOperation.identifier = anIdentifier;
thisDataLoaderOperation.imageURL = anImageURL;

return thisDataLoaderOperation;
}

- (void) main
{
isRunning = YES;
hasStarted = YES;

if ([callbackTarget respondsToSelector:callbackSelector]) {
NSData *data = [NSData dataWithContentsOfURL:imageURL];
[callbackTarget performSelector:callbackSelector withObject:data];
}

isRunning = NO;
}

@end


So it calls the sendImageBack: function in the ImageLoader class:

- (void) sendImageBack:(NSData*)data
{
if (!data) {
return;
}

[data retain];
[self addImageDataToCache:data forURL:imageURL];

UIImage *thisImage = [UIImage imageWithData:data];

if ([callbackTarget respondsToSelector:callbackSelector]) {
[callbackTarget performSelector:callbackSelector withObject:thisImage];
}
}


finally, in the UITableCell class I have:


- (void) setupImage:(UIImage*)anImage
{
avatarImageView.image = anImage;
}




Sorry if that doesn't fill in all the holes but it should help you out with what you may want for your flow.

Isorinu'
Oct 16, 2008, 07:52 AM
hellou youPhone,
i have a little problem with loading remote images and i google it and stumbled over your post about this.

you said that you made a custom NSOperation in the ImageLoader class which will download the data and send that data back to the ImageLoader class through a callback function.

The custom operation in your code above is DataLoaderOperation? because i can't get my head round this...

thank you :)

youPhone
Oct 16, 2008, 09:45 AM
There are two classes. The ImageLoader class (which is a subclass of NSObject), and there is a DataLoaderOperation class (which is a subclass of NSOperation).

So I already posted part of the code, but here's the complete code for the DataLoaderOperation class:

DataLoaderOperation.h

#import <UIKit/UIKit.h>


@interface DataLoaderOperation : NSOperation
{
NSInteger identifier;
id callbackTarget;
SEL callbackSelector;
NSURL *imageURL;
}

@property (assign) id callbackTarget;
@property (assign) SEL callbackSelector;
@property (assign) NSInteger identifier;
@property (nonatomic, retain) NSURL *imageURL;

+ (id) queueDataLoadWithURL:(NSURL*)imageURL withIdentifier:(NSInteger)identifier withCallbackTarget:(id)target withCallbackSelector:(SEL)selector;

@end


DataLoaderOperation.m

#import "DataLoaderOperation.h"

@implementation DataLoaderOperation

@synthesize callbackTarget;
@synthesize callbackSelector;
@synthesize identifier;
@synthesize imageURL;

+ (id) queueDataLoadWithURL:(NSURL*)anImageURL withIdentifier:(NSInteger)anIdentifier withCallbackTarget:(id)aTarget withCallbackSelector:(SEL)aSelector
{
NSLog(@"Made an NSOperation withId:(%d)", anIdentifier);
DataLoaderOperation *thisDataLoaderOperation = [[[DataLoaderOperation alloc] init] autorelease];

thisDataLoaderOperation.callbackTarget = aTarget;
thisDataLoaderOperation.callbackSelector = aSelector;
thisDataLoaderOperation.identifier = anIdentifier;
thisDataLoaderOperation.imageURL = anImageURL;

return thisDataLoaderOperation;
}

- (void) main
{
if ([callbackTarget respondsToSelector:callbackSelector]) {
NSData *data = [NSData dataWithContentsOfURL:imageURL];
[callbackTarget performSelector:callbackSelector withObject:data];
}
}

@end

Isorinu'
Oct 17, 2008, 04:03 AM
Thanks for the quick reply!

i made the modifications in my classes but i have some questions..first my code:


@interface ImageLoader: NSObject
{
id callbackTarget;
SEL callbackSelector;
NSURL *imageURL;
}

@property (assign) id callbackTarget;
@property (assign) SEL callbackSelector;
@property (nonatomic, retain) NSURL *imageURL;

+ (void)loadImageFromURL:(NSURL *)imageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector;
- (void)sendImageBack:(NSData *) data;
- (void) addImageDataToCache:(NSData *)aData forURL:(NSURL *) aImageURL;


@implementation ImageLoader
@synthesize callbackTarget;
@synthesize callbackSelector;
@synthesize imageURL;

static NSOperationQueue *imageLoadingQueue;
static NSMutableDictionary *imageCache;

+ (void)loadImageFromURL:(NSURL *)anImageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector
{
//error identifierCounter undeclared first use
DataLoaderOperation *op = [DataLoaderOperation queueDataLoadWithURL:anImageURL withIdentifier:identifierCounter withCallbackTarget:self withCallbackSelector:@selector(sendImageBack:)];
[imageLoadingQueue addOperation:op];
}

- (void) sendImageBack:(NSData*)data
{
if (!data) {
return;
}

[data retain];
[self addImageDataToCache:data forURL:imageURL];

UIImage *thisImage = [UIImage imageWithData:data];

if ([callbackTarget respondsToSelector:callbackSelector]) {
[callbackTarget performSelector:callbackSelector withObject:thisImage];
}
}

- (void) addImageDataToCache:(NSData *)aData forURL:(NSURL *) aImageURL
{
[imageCache setObject:aData forKey:aImageURL];
}


DataLoaderOperation is what you've send yesterday.
In ImageLoader's function loadImageFromURL I don;t know where you get identifierCounter from.

My custom TableCell looks like this:

@interface FeedTableViewCell : UITableViewCell {

UILabel *titleLabel;
UILabel *dateLabel;
NSMutableString *URL;
NSMutableString *imgSrc;
UIImageView *img;

}

- (void) setData:(NSDictionary *) aFeed;


And in the TableViewCell.m i first initialize the UIImageView like this with a no-image.png


UIImage *noPicImageSmall = [[UIImage imageNamed:@"noFeedImg.png"] retain];
self.img = [[UIImageView alloc] initWithImage:noPicImageSmall];
[myContentView addSubview: self.img];



The setupImage function from the TableViewCell i should call it in the TableViewController's function (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath?

Thank you for your help

Isorinu'
Oct 17, 2008, 09:18 AM
Me again,
i can;t understand why in my ImageLoader class after adding the image to the OperationQUeue the other functions don;t run...
The indetifier in the DataLoaderOperation i made it equal to Identifier:[self urlToHashString: anImageURL] but still nothing...and i don't understand why it doesn't call the other functions...i doing something wrong and i don;t know what.... :(

@implementation ImageLoader
@synthesize callbackTarget;
@synthesize callbackSelector;
@synthesize imageURL;

static NSOperationQueue *imageLoadingQueue;
static NSMutableDictionary *imageCache;

+ (void)loadImageFromURL:(NSURL *)anImageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector
{
//error identifierCounter undevlared first use
NSLog(@"ImageLoader.m anImageURL = %@", anImageURL);
DataLoaderOperation *op = [DataLoaderOperation queueDataLoadWithURL:anImageURL withIdentifier:[self urlToHashString: anImageURL] withCallbackTarget:self withCallbackSelector:@selector(sendImageBack:)];
[imageLoadingQueue addOperation:op];
}

+ (NSString*) urlToHashString:(NSURL*)aURL
{
return [NSString stringWithFormat:@"%U",[[aURL absoluteString] hash]];
}

- (NSData*) dataForURL:(NSURL*)aURL
{
NSLog(@"ImageLoader.m dataForURL %@", aURL);
return [imageCache valueForKey:[ImageLoader urlToHashString:aURL]];
}

- (void) addImageDataToCache:(NSData*)aDatum forURL:(NSURL*)aURL
{
NSLog(@"ImageLoader.m am adaugat in cache imaginea pentru url= %@", aURL);
[imageCache setValue:aDatum forKey:[ImageLoader urlToHashString:aURL]];
}

- (void) sendImageBack:(NSData*)data
{
if (!data) {
return;
}

[data retain];
[self addImageDataToCache:data forURL:imageURL];

UIImage *thisImage = [UIImage imageWithData:data];
if ([callbackTarget respondsToSelector:callbackSelector]) {
[callbackTarget performSelector:callbackSelector withObject:thisImage];
}
}

youPhone
Oct 17, 2008, 09:24 AM
In ImageLoader, add a new static variable

static NSInteger identifierCounter;

It looks like you have set up self.img to be your UIImageView. So you should be able to call something like this:
[imageLoader loadImageWithURL:url withCallbackTarget:self.img withCallbackSelector:@selector(setImage:)];

Next, I've always had to make an instance variable in any objects that require an ImageLoader because if your ImageLoader is autoreleased (or released), then it will try to perform the callback method on a released object.

Lastly, look at one of my previous posts where I talked about a "urlToHashString:" method. You're going to want to use a hash value instead of the actual url for the key when doing this call:
[imageCache setObject:aData forKey:aImageURL];

youPhone
Oct 17, 2008, 09:31 AM
Change
+ (void)loadImageFromURL:(NSURL *)anImageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector

to an instance method:
- (void)loadImageFromURL:(NSURL *)anImageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector

In your class that uses the image loader, make an instance variable:
ImageLoader *imageLoader;

Then:
imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:yourURL withCallbackTarget:yourObject withCallbackSelector:@selector(yourMethod:)];

youPhone
Oct 17, 2008, 09:35 AM
Also, you need to set up all of the instance variables in ImageLoader in the
loadImageFromURL:withCallbackTarget:withCallbackSelector: method, because imageURL, callbackTarget, and callbackSelector are all undefined when sendImageBack: is being called

Isorinu'
Oct 17, 2008, 09:44 AM
I made all the modifications you suggested. but i receive an error. "terminating due to uncaught exception"

in my custom table cell class i have this function which sets the custom data to the cell:

-(void) setData:(NSDictionary *) dict
{
self.titleLabel.text = [ dict objectForKey:@"title" ];
self.dateLabel.text = [ dict objectForKey:@"pubDate" ];
self.URL = [ dict objectForKey:@"link" ];
NSMutableString *temp = [[NSMutableString alloc] initWithString:@"http://www.wai.de/emailsJPG/114x120/"];
[temp appendString:[dict objectForKey:@"description"]];
self.imgSrc = temp;

NSURL *imgUrl = [[NSURL alloc] init];
imgUrl = [NSURL URLWithString:@"http://www.wai.de/emailsJPG/114x120/372373.jpg"];

imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self.img withCallbackSelector:@selector(setupImage:)];

}

this is the place where i need the image loader ..also in this class(after reading your other posts) i added the setupImage method:

- (void) setupImage:(UIImage *) anImage
{
[img setImage:anImage];
}

my identifierCounter in the ImageLoader class i initialized it with the int version of the url like in your previous post...

identifierCounter = [self urlToHashString: anImageURL];
is it ok?

thank you for all your help

youPhone
Oct 17, 2008, 09:55 AM
The identifier isn't a big deal. I don't even really use it other than to log how many different operations I've made at this point.

I need more information on that error you're receiving. Does it say anything else useful? Could you post the error?

You may need to start using some breakpoints or NSLog() to find out where your program is getting to and what is causing the exception if the error doesn't show anything useful.

Isorinu'
Oct 17, 2008, 10:01 AM
this is what appears in the debugger:

*** -[ImageLoader urlToHashString:]: unrecognized selector sent to instance 0x489ad0
2008-10-17 18:00:25.257 WAIRSS1.2[4783:20b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[ImageLoader urlToHashString:]: unrecognized selector sent to instance 0x489ad0'

Isorinu'
Oct 17, 2008, 10:05 AM
Also, you need to set up all of the instance variables in ImageLoader in the
loadImageFromURL:withCallbackTarget:withCallbackSelector: method, because imageURL, callbackTarget, and callbackSelector are all undefined when sendImageBack: is being called

this is my current ImageLoader class:

@implementation ImageLoader
@synthesize callbackTarget;
@synthesize callbackSelector;
@synthesize imageURL;

static NSOperationQueue *imageLoadingQueue;
static NSMutableDictionary *imageCache;
static NSInteger identifierCounter;

- (void)loadImageFromURL:(NSURL *)anImageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector
{
callbackTarget = target;
callbackSelector = selector;
imageURL = anImageURL;

NSLog(@"ImageLoader.m anImageURL = %@", imageURL);

identifierCounter = [self urlToHashString: imageURL];
DataLoaderOperation *op = [DataLoaderOperation queueDataLoadWithURL:imageURL withIdentifier:identifierCounter withCallbackTarget:self withCallbackSelector:@selector(sendImageBack:)];
[imageLoadingQueue addOperation:op];
}

+ (NSString*) urlToHashString:(NSURL*)aURL
{
return [NSString stringWithFormat:@"%U",[[aURL absoluteString] hash]];
}

- (NSData*) dataForURL:(NSURL*)aURL
{
NSLog(@"ImageLoader.m dataForURL %@", aURL);
return [imageCache valueForKey:[ImageLoader urlToHashString:aURL]];
}

- (void) addImageDataToCache:(NSData*)aDatum forURL:(NSURL*)aURL
{
NSLog(@"ImageLoader.m am adaugat in cache imaginea pentru url= %@", aURL);
[imageCache setValue:aDatum forKey:[ImageLoader urlToHashString:aURL]];
}

- (void) sendImageBack:(NSData*)data
{
if (!data) {
return;
}

[data retain];
[self addImageDataToCache:data forURL:imageURL];

UIImage *thisImage = [UIImage imageWithData:data];
if ([callbackTarget respondsToSelector:callbackSelector]) {
[callbackTarget performSelector:callbackSelector withObject:thisImage];
}
}

youPhone
Oct 17, 2008, 10:16 AM
this is what appears in the debugger:

*** -[ImageLoader urlToHashString:]: unrecognized selector sent to instance 0x489ad0
2008-10-17 18:00:25.257 WAIRSS1.2[4783:20b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[ImageLoader urlToHashString:]: unrecognized selector sent to instance 0x489ad0'

Have you added the urlToHashString function to your .h file?

Isorinu'
Oct 17, 2008, 10:18 AM
this is my ImageLoader.h


@interface ImageLoader: NSObject
{
id callbackTarget;
SEL callbackSelector;
NSURL *imageURL;
}

@property (assign) id callbackTarget;
@property (assign) SEL callbackSelector;
@property (nonatomic, retain) NSURL *imageURL;

- (void)loadImageFromURL:(NSURL *)anImageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector;
- (void) sendImageBack:(NSData *) data;
- (void) addImageDataToCache:(NSData *)aData forURL:(NSURL *) aImageURL;
+ (NSString*) urlToHashString:(NSURL*)aURL;
- (NSData*) dataForURL:(NSURL*)aURL;

youPhone
Oct 17, 2008, 10:23 AM
change the line:
identifierCounter = [self urlToHashString: imageURL];

You can start with just making it
identifierCounter = 0;

If that doesn't work, you need to identify the line that is throwing that error by putting NSLog()'s before/after all of them or use breakpoints.

Isorinu'
Oct 17, 2008, 10:30 AM
change the line:
identifierCounter = [self urlToHashString: imageURL];

You can start with just making it
identifierCounter = 0;

If that doesn't work, you need to identify the line that is throwing that error by putting NSLog()'s before/after all of them or use breakpoints.

after i made identifierCounter = 0; no error appeared but i added NSLog(@"send image back"); to test if the function sendImageBack is called
and the line doesn't appear in the console...

- (void) sendImageBack:(NSData*)data
{
if (!data) {
return;
}
NSLog(@"send image back"); //added this to test if the function is called
[data retain];
[self addImageDataToCache:data forURL:imageURL];

UIImage *thisImage = [UIImage imageWithData:data];
if ([callbackTarget respondsToSelector:callbackSelector]) {
[callbackTarget performSelector:callbackSelector withObject:thisImage];
}
}

youPhone
Oct 17, 2008, 10:38 AM
So put NSLog()'s in all of your functions and see what's getting hung up. Seems like standard debugging at this point I would say.

Isorinu'
Oct 17, 2008, 10:42 AM
So put NSLog()'s in all of your functions and see what's getting hung up. Seems like standard debugging at this point I would say.

I've done that..the only NSLog() that appears is the one that you put originally in the queueDataLoadWithURL:(NSURL*)anImageURL withIdentifier:(NSInteger)anIdentifier withCallbackTarget:(id)aTarget withCallbackSelector:(SEL)aSelector more exactly NSLog(@"Made an NSOperation withId:(%d)", anIdentifier);

the rest of them do not appear..and this is really wierd..

youPhone
Oct 17, 2008, 11:11 AM
Alright, I may see the problem.

Here's a tip when debugging, you can do
if (someObject == nil) {
NSLog(@"someObject exists");
}

You can also do:
NSLog(@"someObject's memory address: <%U>", someObject);

Your imageCache object has not been initialized anywhere. So if you do:
NSLog(@"About to add an op to imageLoadingQueue <%U>", imageLoadingQueue);
[imageLoadingQueue addOperation:op];

You'll see in the log that imageLoadingQueue will show up as nil (NULL or 0)


So now add an init function in ImageLoader:
- (id) init
{
if (self = [super init]) {
if (imageCache == nil) {
identifierCounter = 0;

imageLoadingQueue = [[NSOperationQueue alloc] init];
[imageLoadingQueue setMaxConcurrentOperationCount:2];
}
}
return self;
}

And when you run it again, your imageLoadingCache will have an address in the log line.

Isorinu'
Oct 20, 2008, 08:55 AM
Alright, I may see the problem.

Here's a tip when debugging, you can do
if (someObject == nil) {
NSLog(@"someObject exists");
}

You can also do:
NSLog(@"someObject's memory address: <%U>", someObject);

Your imageCache object has not been initialized anywhere. So if you do:
NSLog(@"About to add an op to imageLoadingQueue <%U>", imageLoadingQueue);
[imageLoadingQueue addOperation:op];

You'll see in the log that imageLoadingQueue will show up as nil (NULL or 0)


So now add an init function in ImageLoader:
- (id) init
{
if (self = [super init]) {
if (imageCache == nil) {
identifierCounter = 0;

imageLoadingQueue = [[NSOperationQueue alloc] init];
[imageLoadingQueue setMaxConcurrentOperationCount:2];
}
}
return self;
}

And when you run it again, your imageLoadingCache will have an address in the log line.

Hello Iphone,
i made what you said..now the NSLogs that i put in all the functions in ImageLoader, DataLoaderOperation and in my FeedTableViewCell classes appear..but the images aren't changed...:confused:

i don't know what tot do next... :(

youPhone
Oct 20, 2008, 09:10 AM
Is what is being sent back an image? Are you sending the image to a UIImageView and the setImage: to the method?


Wherever you're calling loadImageFromURL:withCallbackTarget:withCallbackSelector: you might want to try changing the selector to a custom function like:

- (void) testCallbackSelector:(UIImage*)image
{
NSLog(@"testCallbackSelector received image with address <%U>", image);
}

You might even use a test image in that function instead of the image you received to try and set the cell with an image.


Walk through all the steps. Make sure everything is initialized and you're getting what you want at each step. It took me quite a while to get everything working just right.

anim510
Oct 20, 2008, 09:59 PM
Hello guys,

Could you send me all of your example codes? because i feel very confused about your talking and uncompleting codes. Your discussion is very helpful for me so I wish I could learn something here from you guys, thanks so much buddies!

;):)
My email is: anim510@gmail.com

Thanks again.

youPhone
Oct 21, 2008, 12:59 AM
Could you send me all of your example codes?

I don't have any example code really.

This person has some example code for doing an NSOperation
http://forums.macrumors.com/showthread.php?t=571887&highlight=nsoperation

And then I have modified and built on that as I explain in this thread.

Read and understand posts #1 through #19 of this thread and that should be enough to get you going

anim510
Oct 21, 2008, 09:24 AM
I don't have any example code really.

This person has some example code for doing an NSOperation
http://forums.macrumors.com/showthread.php?t=571887&highlight=nsoperation

And then I have modified and built on that as I explain in this thread.

Read and understand posts #1 through #19 of this thread and that should be enough to get you going

OK, thanks for your help.

I will read these posts again.

:D

anim510
Oct 22, 2008, 10:01 AM
I'm struggling to come up with an efficient and performant way of loading remote images (over a local wifi network) to be used as images in a UITableViewCell.

What I'm basically trying to achieve is something similar to what the Apple iTunes Remote app does - when you browse a list of albums, it retrieves the artwork from the machine running iTunes and displays it on the left. The Remote app obviously does this asynchronously as the artwork doesn't appear immediately the first time you view a row.

Here's what I have so far: a custom UITableViewCell subclass that represents an Album; it has two properties, album name and album artwork URL, and two views, a UILabel and a UIImageView. When the album name is set, the UILabel is updated. This works just fine. When the artwork URL is set, the idea is that an asynchronous HTTP connection downloads the artwork an dupdates the UIImageView when its finished.

Here is my code for a custom AsyncArtworkFetcher class which, given a URL, downloads the artwork asynchronously and invokes a method on a delegate object when finished:


@implementation AsyncArtworkFetcher

@synthesize delegate;
@synthesize url;
@synthesize userData;

- (void)fetch;
{
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if(connection) {
receivedData = [[NSMutableData data] retain];
}
}

#pragma mark NSURLConnection Delegate Methods

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
UIImage *downloadedImage = [UIImage imageWithData:receivedData];
SEL delegateSelector = @selector(artworkFetcher:didFinish:);
if([delegate respondsToSelector:delegateSelector]) {
[delegate performSelector:delegateSelector withObject:self withObject:downloadedImage];
}
[receivedData release];
[connection release];
}

@end


And here's the relevant part of my custom table view cell class:


- (void)setArtworkURL:(NSURL *)newURL;
{
if(newURL != artworkURL) {
[artworkURL release];
artworkURL = [newURL retain];
AsyncArtworkFetcher *artworkFetcher = [[AsyncArtworkFetcher alloc] init];
artworkFetcher.url = artworkURL;
artworkFetcher.delegate = self;
[artworkFetcher fetch];
}
}

#pragma mark AsyncArtworkFetcher Delegate Methods

- (void)artworkFetcher:(AsyncArtworkFetcher *)fetcher didFinish:(UIImage *)artworkImage;
{
albumArtView.image = artworkImage;
[self setNeedsDisplay];
}


The main issue with this approach is down to the way in which UITableViewCell objects are reused. As you scroll through the list, when a cell is reused it will display the artwork for another album until its updated (its only briefly but it just looks wrong). One approach would be to have a unique cell for each album but this defeats the whole point of reusable cells.

Apart from this one issue, this works well enough in the simulator, but running on my actual iPhone it just locks the phone up, probably because of the number of asynchronous requests it's firing off at once. I guess I need to cache the downloaded image in some way (although I thought NSURLConnection dealt with caching automatically - perhaps I'm not configuring it correctly).

Any suggestions on a better way of going about this? Maybe I'm missing a really obvious technique - it must be possible, the Apple Remote app shows it can be done without any serious performance penalties.

Hey guys. How it going on ? It works now ? I want to get more information about this. I have read these posts more than one time.

I have the problem that is every time scrolling the cell lists, it will be lagging. It locks my phone up, because of it downloading the image from the internet.

Isorinu'
Oct 30, 2008, 01:53 PM
Hello again youPhone,
i looked over my code several times and everything stopes in the ImageLoader class when it verifies if the callback target responds to the selector.

- (void) sendImageBack:(NSData*)data
{
NSLog(@"Send image back function. Addres for data: <%U>", data);
if (!data) {
return;
}
NSLog(@"send image back"); //added this to test if the function is called
//[data retain];
NSLog(@"address for data = <%U>", data);
[self addImageDataToCache:data forURL:imageURL];

UIImage *thisImage = [UIImage imageWithData:data];
//UIImage *thisImage = [UIImage imageNamed:@"wai_news_logo_loading.png"];
//NSLog(@"Address for thisImage = <%U>", thisImage);

if ([callbackTarget respondsToSelector:callbackSelector]) {
NSLog(@"sendImageBack inside IF");
[callbackTarget performSelector:callbackSelector withObject:thisImage];
}
//NSLog(@"Address fot thisImage= <%U>", thisImage);
}

the [callbackTarget respondsToSelector:callbackSelector] in the last if always returns false so the image isn't sent back. i commented the if and i received the fallowing error:
*** -[UIImageView testCallbackSelector:]: unrecognized selector sent to instance 0x4bb0a0


the test callback selector in this case is:

-(void) testCallbackSelector: (UIImage *) image
{
NSLog(@"testCallbackSelector received image with address <%U>", image);
}

do you have any idea why the callback target doesn't respond to the selector?

thank you in advance,
Sorin

youPhone
Oct 30, 2008, 02:00 PM
On the call that looks like this:
[imageLoader loadImageFromURL:yourURL withCallbackTarget:yourObject withCallbackSelector:@selector(yourMethod:)];
Verify that you have properly set 'yourObject' and 'yourMethod:'

Also, make sure whatever the object is that you're using (yourObject), make sure that 'yourMethod:' is defined by that object, such as adding it to the .h file.

If you're still having trouble, post the code that is making the call (it should look similar to the above code).

Isorinu'
Oct 30, 2008, 02:57 PM
thank you for your quick answer youPhone.
My custom UITableViewCell looks like this.
#import <UIKit/UIKit.h>
#import "Feed.h"
#import "ImageLoader.h"


@interface FeedTableViewCell : UITableViewCell {

UILabel *titleLabel;
UILabel *dateLabel;
NSMutableString *URL;
NSMutableString *imgSrc;
UIImageView *img;
ImageLoader *imageLoader;

}

- (void) setData:(NSDictionary *) aFeed;
- (void) testCallbackSelector: (UIImage *) image;
- (void) setupImage:(UIImage *)anImage;
- (UILabel *) newLabelWithPrimaryColor:(UIColor *) primaryColor selectedColor: (UIColor *) selectedColor fontSize:(CGFloat) fontSIze bold:(BOOL) bold;


@property(nonatomic, retain) UILabel *titleLabel;
@property(nonatomic, retain) UILabel *dateLabel;
@property(nonatomic, retain) NSMutableString *URL;
@property(nonatomic, retain) NSMutableString *imgSrc;
@property(nonatomic, retain) UIImageView *img;
@property(nonatomic, retain) ImageLoader *imageLoader;

@end

in my FeedTableViewCell. i have:
#import "FeedTableViewCell.h"
#import "ImageLoader.h"


@implementation FeedTableViewCell
@synthesize titleLabel, dateLabel, URL, imgSrc, img, imageLoader;

...

-(void) setData:(NSDictionary *) dict
{
self.titleLabel.text = [ dict objectForKey:@"title" ];
self.dateLabel.text = [ dict objectForKey:@"pubDate" ];
self.URL = [ dict objectForKey:@"link" ];
NSMutableString *temp = [[NSMutableString alloc] initWithString:@"http://www.wai.de/emailsJPG/114x120/"];
[temp appendString:[dict objectForKey:@"description"]];
// clean up the link - get rid of spaces, returns, and tabs...
temp = [temp stringByReplacingOccurrencesOfString:@" " withString:@""];
temp = [temp stringByReplacingOccurrencesOfString:@"\n" withString:@""];
temp = [temp stringByReplacingOccurrencesOfString:@"\t" withString:@""];
temp = [[temp componentsSeparatedByString:@"<"] objectAtIndex:0];

NSURL *imgurl = [NSURL URLWithString: temp];

imageLoader = [[ImageLoader alloc] init];
//[imageLoader loadImageFromURL:imgurl withCallbackTarget:img withCallbackSelector:@selector(setupImage:)];
[imageLoader loadImageFromURL:imgurl withCallbackTarget:self.img withCallbackSelector:@selector(testCallbackSelector:)];
}

- (void) setupImage:(UIImage *) anImage
{
NSLog(@"Setup Image in table cell");

UIImage *loadImage = [[UIImage alloc] init];
loadImage = anImage;

[img setImage:loadImage];
}

-(void) testCallbackSelector: (UIImage *) image
{
NSLog(@"testCallbackSelector received image with address <%U>", image);
}


the setData function is called in the FeedTableViewController.m at - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

static NSString *CellIdentifier = @"MyIdentifier";

FeedTableViewCell *cell = (FeedTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[FeedTableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

}

// Set up the cell
int storyIndex = [indexPath indexAtPosition: [indexPath length] - 1];

NSDictionary *itemAtIndex = (NSDictionary *)[stories objectAtIndex: storyIndex];
[cell setData:itemAtIndex];
return cell;
}


thank you for all the help!
Sorin

youPhone
Oct 30, 2008, 03:09 PM
I think this:
[imageLoader loadImageFromURL:imgurl withCallbackTarget:self.img withCallbackSelector:@selector(testCallbackSelector:)];

should be:
[imageLoader loadImageFromURL:imgurl withCallbackTarget:self withCallbackSelector:@selector(testCallbackSelector:)];

Isorinu'
Oct 30, 2008, 03:24 PM
Thank you! thank you! thank you! thank you! thank you! thank you! thank you!
you are the best! it's working perfectly!

can u please explain why that modification was necessary?

thanK you for all your time and patience :) you made me really happy!

youPhone
Oct 30, 2008, 03:49 PM
In this line:
[imageLoader loadImageFromURL:imgurl withCallbackTarget:self withCallbackSelector:@selector(testCallbackSelector:)];

When you had the self.img as the callback target, it was trying to send the selector 'testCallbackSelector' to 'self.img' which is a UIImageView object as specified in your code. As it said in the error message, 'self.img' does not have a selector 'testCallbackSelector'

The testCallbackSelector is a custom method that you've added to your custom FeedTableViewCell class, therefore you need to call testCallbackSelector on your FeedTableViewCell instance (which is 'self') rather than the UIImageView ('self.img').

Wunk
Nov 17, 2008, 12:32 PM
Great thread, this helped me a lot with making images load smoother in a tableviewcontroller.. (filling it with data through a JSON framework)

I am however running into same issue with the hashtostring function, the debugger gives:

2008-11-17 19:17:38.661 Kookjij-Menus[12588:20b] ImageLoader.m anImageURL = http://static.kookjij.nl//upload//0000/7876/00007876-60x60.jpg
2008-11-17 19:17:38.664 Kookjij-Menus[12588:20b] *** -[ImageLoader urlToHashString:]: unrecognized selector sent to instance 0x3824d0


Setting the identifiercounter = 0; solves it, but from what I understood it can cause issues with longer URL's ?

And what's the best way to unload or cache an image into memory ?, at the moment when a cell is re-used while scrolling, the old image displays until the new one is loaded, this is noticable when scrolling back and forward, this will temporarily display the wrong image in a cell when it pops back into view..

I'm doing a #import DataLoaderOperation.h class from the ImageLoader.m, file, which is in turn called from the ViewImageCell subclass of a TableViewController.., as with the above example it's also called from a setData function and a setupImage function:


-(void)setData:(NSDictionary *)dict {
self.titleLabel.text = [dict objectForKey:@"naam"];
self.urlLabel.text = [dict objectForKey:@"description"];
self.itemID = (int)[dict objectForKey:@"id"];

NSURL *imgUrl = [[NSURL alloc] init];
imgUrl = [NSURL URLWithString:[dict objectForKey:@"image"]];

imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];

}

- (void) setupImage:(UIImage *) anImage
{
NSLog(@"Setup Image in table cell");
UIImage *loadImage = [[UIImage alloc] init];
loadImage = anImage;

[imageView setImage:loadImage];
}

youPhone
Nov 17, 2008, 01:02 PM
I'm not sure how setting the indentifierCounter = 0 is solving the "unrecognized selector" error you're getting. Can you post the actual line that is throwing the error?

the old image displays until the new one is loaded

In the protocol method:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

Try setting the imageView.image to nil like:
MyCellClass *cell = (MyCellClass*)[tableView dequeueReusableCellWithIdentifier:kMyCellIdentifier];
if (cell == nil) {
cell = [[MyCellClass alloc] initWithFrame:myFrame reuseIdentifier:kMyCellIdentifier];
} else {
// Reset the cell's image so that it doesn't repeat images
[cell.myImageView setImage:nil];
}

Wunk
Nov 17, 2008, 03:58 PM
I'm not sure how setting the indentifierCounter = 0 is solving the "unrecognized selector" error you're getting. Can you post the actual line that is throwing the error?


Try setting the imageView.image to nil like:


Hey that works great, thanks :-)


The part that throws the error is pretty much the same from the examples:

- (void)loadImageFromURL:(NSURL *)anImageURL withCallbackTarget:(id)target withCallbackSelector:(SEL) selector
{
callbackTarget = target;
callbackSelector = selector;
imageURL = anImageURL;

NSLog(@"ImageLoader.m anImageURL = %@", imageURL);

// turned off the hashing, crashes the app
// identifierCounter = [self urlToHashString: imageURL];
identifierCounter = 0;
NSLog(@"Image naar interface gestuurd");
DataLoaderOperation *op = [DataLoaderOperation queueDataLoadWithURL:imageURL withIdentifier:identifierCounter withCallbackTarget:self withCallbackSelector:@selector(sendImageBack:)];
[imageLoadingQueue addOperation:op];
}


Uncommenting the above line will throw the exception, this is my urlToHashString function (it's all pretty much as in the threads):

+ (NSString*) urlToHashString:(NSURL*)aURL
{
return [NSString stringWithFormat:@"%U",[[aURL absoluteString] hash]];
}


With identifierCounter = 0; it'll log a tidy debugger output:

2008-11-17 22:53:26.214 Kookjij-Menus[13954:a703] Send image back function. Addres for data: <3841952>
2008-11-17 22:53:26.215 Kookjij-Menus[13954:a703] send image back
2008-11-17 22:53:26.218 Kookjij-Menus[13954:a703] address for data = <3841952>
2008-11-17 22:53:26.219 Kookjij-Menus[13954:a703] ImageLoader.m am adaugat in cache imaginea pentru url= http://www.kookjij.mobi//upload//0000/7887/00007887-60x60.jpg
2008-11-17 22:53:26.221 Kookjij-Menus[13954:a703] sendImageBack inside IF
2008-11-17 22:53:26.224 Kookjij-Menus[13954:a703] Setup Image in table cell


But uncommenting the identifierCounter = [self urlToHashString: imageURL]; will throw that exception.., it's all called from the setData function I posted earlier..

youPhone
Nov 17, 2008, 04:07 PM
I see now.

The Identifier is supposed to be an NSInteger, but urlToHashString: returns a string.

Anyway, in my code, I made an identifierCounter static that just increments for each operation that is made. So far I haven't even used the identifier except in testing.

youPhone
Nov 17, 2008, 04:12 PM
I actually have a question for anyone that has made an NSOperationQueue.

I have this piece of code:
NSArray *ops = [myOperationQueue operations];
NSLog(@"operationsQueued:%U", [ops count]);

This will work in the simulator, but it does not work (only shows "operationsQueued:0") on the iphone hardware for me. I'm not sure why this is.

Could it be because this code isn't being called on the main thread?

shailendra
Nov 18, 2008, 03:31 AM
Thanks, it's great thread. It worked in the my application. But while scrolling, it does not scroll very smoothness as i saw in the AppStore & other application.
Please guide me. What i can do to scroll fast.

Wunk
Nov 18, 2008, 11:28 AM
Got it working perfectly so far, thanks for the tips :-)

Also, would it be easy to cache for example the last 10 or 20 images loaded ?, I've been tailing the webserver log when running this app and it's stressing the webserver fairly heavily due to continuous requests for images already retreived when scrolling back and forth in the tableview..

It'll easily generate 5-6 requests per second, which can be a problem if there's a lot of people using the app when it's live (something we must all be aiming for ;-)

Wunk
Nov 18, 2008, 02:41 PM
Also, I seem to have run into a small bug after modifying it so that it will not return a key if there's no image present (makes it a lot faster too), but when scrolling back and forth really fast it'll start putting images in the wrong TableCell (probaply because it's still loading the image while the TableCell is already off screen)

I've tried setting the imageView = nil; when the objectForKey:@"image" == nil, but that will result in re-used cells going permanently blank.., so I made en emptyCallBackSelector which does nothing but throw a NSLog line..

The [dict objectForKey:@"image"] is a method to pull the "image" variable from a JSON page, if the variable doesn't exist it'll make the imageView nil.


-(void)setData:(NSDictionary *)dict {
self.titleLabel.text = [dict objectForKey:@"naam"];
self.urlLabel.text = [dict objectForKey:@"description"];
self.itemID = (int)[dict objectForKey:@"id"];
NSURL *imgUrl = [[NSURL alloc] init];

if ([dict objectForKey:@"image"] == nil) {
NSLog(@"No image found");
imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(emptyCallbackSelector:)];
} else {
NSLog(@"Image found");
imgUrl = [NSURL URLWithString:[dict objectForKey:@"image"]];
imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];
}
}
- (void) setupImage:(UIImage *) anImage
{
UIApplication* app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES; // Activity indicator starten bij laden plaatje
NSLog(@"Setup Image in table cell");
UIImage *loadImage = [[UIImage alloc] init];
loadImage = anImage;
[imageView setImage:loadImage];
app.networkActivityIndicatorVisible = NO; // indicator uit als plaatje geladen is

}

shailendra
Nov 24, 2008, 01:50 AM
Hi, when i scrolling, the images again reload in cell. I don't want to call the same images again. Once it is assigned into cell, it should not call while scrolling.What is the way to store images so that same image url need not to call again.

Wunk
Nov 24, 2008, 12:28 PM
Anyone else seeing issues with these threaded imageloader functions ?, they worked almost flawlessly in the 2.1 firmware release on the iPhone itself, but it's calling some bad mojo in the 2.2 firmware.

If I load a local file through the ImageLoader (gif) it works fine, but the moment it starts loading external images it starts borking. Strange thing is that I can't reproduce this on the simulator..

So this works (this is called when no remote image is found):

NSString *path = [[NSBundle mainBundle] pathForResource:@"blank-60x60" ofType:@"gif"];
imgUrl = [NSURL fileURLWithPath: path];
imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];


And this throws a 'Program received signal: “EXC_BAD_ACCESS”.' error:


imgUrl = [NSURL URLWithString:[dict objectForKey:@"image"]];
imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];


Anyone else seeing this ?




edit: never mind, I found a [blah release] in my code where it wasn't alloc'ed in the first place, removing that fixed my problem..

Please walk along, nothing to see here ;)

amitATwap
Dec 1, 2008, 08:17 PM
I followed your code, used ImageLoader, DataLoaderOperation class.
but the scrolling is not that smooth, everytime scrolling happen image from the URL loaded.

you guys getting glassy scrolling...if yes then please share your experience.

Cheers,
Amit

debtman7
Feb 16, 2009, 10:54 AM
Ok, so this thread is a bit old, but it's about the only good example that turns up for remote images in a tableview.

So, just to chime in, I've played with it and unless the folks here know something I don't, I've come to the conclusion that the approach described here just isn't workable. The problem I ran into is that by the time the image downloads and performs the callback on the table cell, it's quite possible that that cell has already been reused (i.e. the user is scrolling) and the image pops up in the wrong place. In addition, since this code handles image loading during the cell setup phase, it means it will try to load images even if the user is quickly scrolling through the list, when we really don't need to show them, and this causes poor performance on scrolling since we're trying to update cell images very quickly during a fast scroll.

When you look at it, we don't need images when the cell is whizzing by. We just need them when the user is looking at a row of cells. So, I've played around with another approach that seems workable.

What I'm doing right now is implementing a custom ImageCellView. It really has almost no custom code, except it adds an imageSrc property. When the cell is created in the tableviewcontroller, it sets this to the url for the image.

To load images, I'm implementing

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

When that fires it fetches all the visible cells from the table, and loads data from their imageSrc property into the cell image and caches it. And that's about it.

The advantage here is that we're not loading any images during a scroll. We're not using any callbacks on cells which may be reused. Performance is *much* better this way and it avoids caching any unnecessary items saving memory (do we really want to load and cache hundreds of images for rows that users zoom right by?).

I need to hash out some more implementation details, most notably make it threaded since my current minimalistic implementation will block the UI. I'm going to have it pass off the image loading to an NSOperation class like the one described here, but only once the scrolling ends. If the scrolling starts again, then cancel all of those requests.

I've also contemplated starting a thread early on to just start going through the list and populating the cache. With this approach it's pretty easy to do, in that thread or in the data loader, just need to check the cache and only fetch if there is a miss. On the flip side though, if your app has a lot of these, that may eat up memory pretty quickly. I'm scaling them to 40x40 before caching, but it ads up.

youPhone
Feb 16, 2009, 11:07 AM
I've done a lot of work to my image loading class since this thread. I never fully described my approach (by giving away the code), because I thought my approach needed work and others might come up with better ideas and shoot them out.

My approach now involves making the ImageLoader a singleton class (a decision I'm still not sure is the right one). Also, instead of sending the UIImage or imageData back in the callback, I made a new class similar to yours that has the image source URL so that you can check to make sure it is to correct image and the correct cell before setting the image.

That's a good idea using scrollViewDidEndDecelerating: to load the cells. I never saw that. Thanks for you input!

youPhone
Feb 16, 2009, 11:09 AM
I also forgot to mention. I think it would be really slick to have sqlite caching of the images (that is, if you want your application to save images to disk for later use). Right now I'm just saving the image data to disk and saving a NSMutableSet to disk to keep track of all the items saved to disk. Though, I may never switch to the sqlite method since I already have this working well enough.

Wunk
Feb 16, 2009, 02:21 PM
I'm actually still programming my app (slowly getting there ;) ) and using your imageloader class as it is earlier in this thread. It's so the best method I've seen code-wise so far..

I was planning on going over it at a later stage to improve performance and iron out the bugs when a user is scrolling pretty fast and a wrong image is loaded, but I haven't come around to it yet.

The Appstore image loader is damn nice, but I haven't seen something like that yet.

I'm going to look at that scrollViewDidEndDecelerating method, that looks like a good performance boost :)

estupefactika
Feb 16, 2009, 04:30 PM
Hi, Im trying to follow the examples explained here. Im not using Interface Builder, it works but I cant put the image in their respective row. All images are display in the first row, im doing some wrong...


In cellForRowAtIndexPath i try to display the image with a UIImageView

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
CGRect formulaRect = CGRectMake(0.0, 0.0, 75.0, 75.0);
UIImage *formulaImage = [UIImage imageNamed:@"blank.png"];
img = [[[UIImageView alloc] initWithFrame:formulaRect] initWithImage:formulaImage];
[cell.contentView addSubview:img];
return cell;
}


Im using the NSOperation to download images and the callback method where it has to display the images, like these guys have explained before:

-(void) testCallbackSelector: (UIImage *) image
{
NSLog(@"testCallbackSelector received image with address <%U>", image);
UIImage *loadImage = [[UIImage alloc] init];
loadImage = image;

[img setImage:loadImage];
}


All images are display in the first row instead of their respective row. Surely Im doing some wrong, any help?

Wunk
Feb 16, 2009, 04:34 PM
I actually implemented it right away, it's a LOT smoother now.., it's still a bit rough at the edges, but scrolling through a table goes as a warm knife through butter ;) ..

What I basically did is split up my setData function (a few posts up) into two parts, one for just the data, and a function calling the imageLoader:


-(void)setData:(NSDictionary *)dict {
self.titleLabel.text = [dict objectForKey:@"naam"];
self.urlLabel.text = [dict objectForKey:@"description"];
self.itemID = (int)[dict objectForKey:@"id"];
}
-(void)setImage:(NSDictionary *)dict {
NSURL *imgUrl = [[NSURL alloc] init];
imageLoader = [[ImageLoader alloc] init];
if ([dict objectForKey:@"image"] == nil) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"blank-60x60" ofType:@"gif"];
imgUrl = [NSURL fileURLWithPath: path];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];
} else {
imgUrl = [NSURL URLWithString:[dict objectForKey:@"image"]];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];
}
}


Then I added the scrollview functions which set a flag based on if the scrollview is starting to move or stopping to move:


- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
NSLog(@"Decelerating");
self.AccelerationQueue = @"1";
[self.menuTabel reloadData]; // reload the table so the images show up
}

-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
NSLog(@"Accelerating");
self.AccelerationQueue = @"0";
}


And I changed the cellForRowAtIndexPath so it looks if the AccelerationQueue flag is set:


- (UITableViewCell *)tableView:(UITableView *)menuTabel cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
ReceptCategorieenImageCell *cell = (ReceptCategorieenImageCell *)[menuTabel dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
reuseIdentifier:CellIdentifier] autorelease];
cell = [[[ReceptCategorieenImageCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
} else {
[cell.imageView setImage:nil];
}
NSDictionary *itemAtIndex = (NSDictionary *)[self.jsonArray objectAtIndex:indexPath.row];
if ( self.AccelerationQueue == @"0" ) {
[cell setData:itemAtIndex]; // only load the text data
} else {
[cell setImage:itemAtIndex]; // set the imagedata after acceleration stopped
[cell setData:itemAtIndex];
};
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;

}


It needs some cleaning up though, the main issue here still, is that it will call the reloadData on the table even if it moved a little bit, so it will cause some overhead on downloading images again after only scrolling a tiny bit.., but it'll lessen the download and CPU load on the iPhone a lot when scrolling through long tables.

edit: the AccelerationQueue variable is nothing more then a NSString in the header with the retain flag set by the way.. (in case you're wondering where that came from)

Wunk
Feb 16, 2009, 04:47 PM
Hi, Im trying to follow the examples explained here. Im not using Interface Builder, it works but I cant put the image in their respective row. All images are display in the first row, im doing some wrong...

All images are display in the first row instead of their respective row. Surely Im doing some wrong, any help?

Try using a seperate UITableViewCell subclass for that, see http://iphone.zcentric.com/2008/08/05/custom-uitableviewcell/ for an example..

You can then setup the position of the image through the - (void)layoutSubviews function with something like
self.imageView.frame = CGRectMake(boundsX + 251, 4, 63, 59); where imageView is the UIImageView outlet

estupefactika
Feb 17, 2009, 10:14 AM
Hi Wunk, Ive just modified my separate UITableViewCell but now I receive an "EXC_BAD_ACCESS" when callback 'testCallbackSelector' has to display images, exactly in line [img setImage:loadImage];, the first times it works, but it fails after. Any idea?


-(void) setData:(NSDictionary *) dict
{

titulo.text=[dict objectForKey:@"titulo"];
entradilla.text=[dict objectForKey:@"entradilla"];

NSURL *imgUrl = [[NSURL alloc] init];
imgUrl = [NSURL URLWithString:[dict objectForKey:@"img"]];

imageLoader = [[ImageLoader alloc] init];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(testCallbackSelector:)];
}


-(void) testCallbackSelector: (UIImage *) image
{
NSLog(@"testCallbackSelector received image with address <%U>", image);
UIImage *loadImage = [[UIImage alloc] init];
loadImage = image;
[img setImage:loadImage];
}

dejo
Feb 17, 2009, 10:19 AM
Not sure on your EXC_BAD_ACCESS, but this right here is a memory leak:
UIImage *loadImage = [[UIImage alloc] init];
loadImage = image;

No need to alloc the loadImage if you're just going to reassign it to image anyways. This is better:
UIImage *loadImage = image;

Wunk
Feb 17, 2009, 10:53 AM
Hi Wunk, Ive just modified my separate UITableViewCell but now I receive an "EXC_BAD_ACCESS" when callback 'testCallbackSelector' has to display images, exactly in line [img setImage:loadImage];, the first times it works, but it fails after. Any idea?



I ran into some issues back there too with the examples here, the testCallbackSelector isn't used except in debugging your code.., but aside from that, if you're receiving a EXC_BAD_ACCESS, then it's most likely because you released something that you still need. So you either shouldn't release it (yet) or alloc it again..

The code I'm using in the ImageCell subclass is:


-(void)setImage:(NSDictionary *)dict {
NSURL *imgUrl = [[NSURL alloc] init];
imageLoader = [[ImageLoader alloc] init];
if ([dict objectForKey:@"image"] == nil) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"blank-60x60" ofType:@"gif"];
imgUrl = [NSURL fileURLWithPath: path];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];
} else {
imgUrl = [NSURL URLWithString:[dict objectForKey:@"image"]];
[imageLoader loadImageFromURL:imgUrl withCallbackTarget:self withCallbackSelector:@selector(setupImage:)];
}

}

- (void) setupImage:(UIImage *) anImage
{
UIApplication* app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES;
UIImage *loadImage = [[UIImage alloc] init];
loadImage = anImage;
[imageView setImage:loadImage];
app.networkActivityIndicatorVisible = NO;
}


Take note that it's calling a blank-60x60.gif when no image is found in the @"image" dict.., so it will probaply crash if it doesn't exist..

youPhone
Feb 17, 2009, 11:27 AM
I just had an idea. I think I'm going to subclass UIImageView to have a class that does all of the asynchronous work behind the scenes so the UIViewController doesn't see what is going on. From the view controller you just

MyImageView *myImageView = [[MyImageView alloc] initWithURL:someURL];


Also, I had advocated for using a method such as:
loadImageFromURL:withCallbackTarget:withCallbackSelector:
But really, I think this should be done with a protocol and delegate, meaning the ImageLoader class can't be a singleton. Right now mine is implemented as a singleton so I'm having to keep using the above method. But if you were to have instances of ImageLoader, you could call it inside MyImageView (as explained above):

ImageLoader *loader = [[[ImageLoader alloc] initWithURL:someURL
withDelegate:self] autorelease];

Now MyImageView will implement whatever protocol method you defined in ImageLoader, such as:
@protocol
- (void) setImageViewWithImage:(UIImage*)someImage fromURL:(NSURL*)someURL;
@end

So in MyImageView:
- (void) setImageViewWithImage:(UIImage*)someImage fromURL:(NSURL*)someURL
{
if ([self.url isEqualToURL:someURL]) {
self.image = someImage;
}
}

I've left isEqualToURL for you to figure out. I don't think I have the best solution for that. Let me know what you're (whoever you are) solution is and we'll see if it's better than mine. I don't want to first corrupt you with my idea. (I basically compare the absoluteString of each URL in isEqualToString -- I suspect the isEqualToString is fairly optimized, so I think it should be a pretty good method)


So now you don't have to keep an instance of ImageLoader around as an instance variable in whatever class you're using it in (should probably only be used in MyImageView in this case - but you could using elsewhere).

estupefactika
Feb 18, 2009, 10:14 AM
Hi Wunk, you was right, I had a released object I still need, it works thanks.

It download the images and display them after, correct.

Now when I do scroll with images already downloaded it does the same operations again, It back to alloc a imageLoader and to download it again. Is this so? or Should I control it?

I had thought to save images in disk once downloaded to not resort to url always, so it would allow to use the app offline.

Im not going to have much images, its a good idea to save in disk?

estupefactika
Feb 26, 2009, 06:08 AM
It is possible to do the same process twice? i.e, once for downloading only the images from my table and a second time to download the rest of images of my xml file.

I would like that while images from the table are being downloaded and displayed, continue downloading the rest of images of my xml in background.

I have done a little test but when it downloads the rest of images I only get some, not everything

Could it have a conflict for using the same ImageLoader class? Any idea? thanks

Edit: Ive create two classes.

i would like to pass a second parameter on callback method:


if ([callbackTarget respondsToSelector:callbackSelector]) {
[callbackTarget performSelector:callbackSelector withObject:thisImage withObject:imageURL];
}


How can I add other param in this method to get the imageUrl too?

-(void) setupImage: (UIImage *) image
{
....

}

kwarren
Feb 26, 2009, 07:56 PM
I mistakenly created a new thread for this problem, but as dejo pointed out, it probably belongs in here.

So, basically I have a glorified RSS reader. It works great: the NSMutableArray of entries shows up in a standard UITableview, and if you select a row, it loads the entry in the app's browser. But, now that I know a little bit more (this is my first app), I'd like to put an image on the side of the table like in the YouTube app or the iPod when you sort by album. Although I haven't messed with it, I mostly understand how you would do this if you were using a static image, but my app will be pulling the thumbnail that is in each RSS entry (for this app it's a YouTube feed). I already have keys/objects set for the title, link, summary, and date, so I imagine I'd have to put in something similar for the image? Can anyone help me further? I've been working on this for a few days, and I'm rather stumped. I've examined the SeismicXML example, but again, those are images in the project file, not being pulled from the web.

Thanks for all the help on this forum. I know I'm not the only one with a question like this, and without Mac Rumors, my first app would have taken far longer!

youPhone
Feb 26, 2009, 10:40 PM
It is possible to do the same process twice? i.e, once for downloading only the images from my table and a second time to download the rest of images of my xml file.

I would like that while images from the table are being downloaded and displayed, continue downloading the rest of images of my xml in background.


I have done a little test but when it downloads the rest of images I only get some, not everything

I'm not exactly sure what you're asking, but if I'm close, then I bet you can do whatever you're talking about.

You probably need to check to make sure the images are actually getting set. It sounds just like a debugging problem. Log every image that's getting downloaded, make sure all are downloaded and that they're valid NSData (or whatever format) and they're getting set as images.



How can I add other param in this method to get the imageUrl too?

Change your selector to:
@selector(setupImage:withURL:)
then this should work:
[callbackTarget performSelector:callbackSelector withObject:thisImage withObject:imageURL]

youPhone
Feb 26, 2009, 10:43 PM
Can anyone help me further?

markj put up a nice example that's complete if I recall correctly. I developed a similar implementation that does some image caching and what not. Anyway, here's the link he posted:
http://www.markj.net/iphone-asynchronous-table-image/

kwarren
Feb 26, 2009, 11:11 PM
markj put up a nice example that's complete if I recall correctly. I developed a similar implementation that does some image caching and what not. Anyway, here's the link he posted:
http://www.markj.net/iphone-asynchronous-table-image/

Thanks, youPhone. However, as far as I understand, my situation is a bit different because the images are part of an RSS feed that is already implemented (successfully) in my app rather than a standalone set of images.

Anyway, if I'm wrong, please correct me, but I think that that's the problem I'm encountering.

youPhone
Feb 27, 2009, 12:46 AM
The thumbnails are URLs, right? markj's implementation takes an image URL, downloads it, and then loads it into a UIImageView (though it's really a subclass of UIImageView).

It sounds like markj's implementation is exactly what you're looking for, or am I missing something?

kwarren
Feb 27, 2009, 12:58 AM
Yeah, they are URLs all thrown together into the one RSS feed. However, since this section of the app is a constantly refreshing (OK, not constantly, but new content is added at least once a day) RSS feed, doesn't that mean that I need to keep the thumbnails with their respective videos? That is, I can't just slap in thumbnail 1's URL in row 1 because eventually the video in row 1 will be in row 2 and so on and so forth. markj's example seems to be dealing with just images, so would that not cause it to not work in my implementation? So I need to somehow use an object/key (like I have for the title, link, and description) to keep everything working as it should? If not, how do I get it so that I may just pull the thumbnails?

That said, perhaps I am not analyzing the situation correctly. As I mentioned, this is my first real app ready for submission, so I could easily be wrong and you could totally be right.

Sorry for all of the questions, but as you can tell, I'm a little lost on this and have already dedicated a few days to this whole thing.

youPhone
Feb 27, 2009, 09:10 AM
Create a Class (call it XMLFeedItem or something more descriptive) that will be the model for your UITableViewCells. Set up your UITableViewCell so that you have a method like
- (void) setupCellWithXMLFeedItem:(XMLFeedItem*)anXMLFeedItem

In that method, you will update all UILabels and other Cell elements including the image. If you're using markj's implementation, you'll have a line in this method that does something like:

- (void) setupCellWithXMLFeedItem:(XMLFeedItem*)anXMLFeedItem
{
// Setup UILabels and what not
...

// Setup image
[self.myAsyncImageView loadImageFromURL:anXMLFeedItem.thumbURL];
}

estupefactika
Mar 2, 2009, 05:06 AM
Change your selector to:
@selector(setupImage:withURL:)
then this should work:
[callbackTarget performSelector:callbackSelector withObject:thisImage withObject:imageURL]

Yes, it works thank you. I had only problems in debug with breakpoints, I dont know why "EXC BAD ACCESS" appeared me sometimes, when I quit breakpoints it works