views:

1058

answers:

8

My code downloads images asynchronously using InternetImage in the tableview: cellForRowAtIndexPath: method by initializing a IntentImage with initWithURL and calling downloadImage. This code runs perfectly after scrolling down to a new cell UITableViewCell in the UITableView, but not before, even though the URL is correct in both cases. None of InternetImage's NSURLConnection delegate methods are called to notify me about success or failure of the connection, as they should be. Calling reloadData:, setNeedsDisplay:, and setNeedsLayout: do nothing since the image fails to download.

Here is the code from my subclass of UiTableViewController:

   - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        Object *object = (Object *)[self.array objectAtIndex:indexPath.row];

        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        if (cell == nil) {
         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"] autorelease];
        }

        cell.textLabel.text = object.attribute;

        if (object.image == nil && object != nil) {
         NSString *URLString = [[MyAppDelegate imageURLFromLink:(object.link) withExtension:@".jpg"]absoluteString];

         InternetImage *asynchImage = [[InternetImage alloc] initWithUrl:URLString object:object];
         [asynchImage downloadImage:self];
        }

        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

        if (object.image != nil && object.thumbnailImage == nil) {

         UIImage *image= (UIImage *) object.image;
         UIImage *thumbnail = [image _imageScaledToSize:CGSizeMake(75.0, 50.0) interpolationQuality:20];
         object.thumbnailImage = thumbnail;
        }
        cell.imageView.image = object.thumbnailImage;

        return cell;
    }
+1  A: 

If you are asking why your images are not downloaded in the background once your table is loaded, it is because tableView:cellForRowAtIndexPath: is only invoked with indexPaths of on-screen cells.

If you want all your images to be downloaded at once rather than when a cell rolls on-screen, write a method which loops through your array property and insantiates InternetImage objects there. A likely place for this could be in viewDidLoad.

Then implement tableView:willDisplayCell:forRowAtIndexPath: in your UITableView delegate and make it responsible for assigning the correct InternetImage object to each cell's imageView property.

firstresponder
My problem is slightly different than this. Loading the images using tableView:cellForRowAtIndexPath: works as it should as soon as I scroll. tableView:cellForRowAtIndexPath: is called for the cells that are on-screen initially, and it properly instantiates an InternetImage object for each with the correct link, yet for some reason the download fails. The NSURLConnection methods, however, do not register either a success or error in the downloading of the initial cells that appear on-screen. I am trying to fix this error, while still loading images with tableView:cellForRowAtIndexPath:.
Matthew Bischoff
OK, so I used this method today and unfortunately it made things worse and reconfirmed this error. Now, instead of only the initial cells having the strange problem with not reporting either a success or failure on the connection, it does that with every single cell - the image download fails, yet it reports neither success nor failure. So somehow this must be a timing problem, I am thinking of sleeping the image downloading for a second and then trying. I'll report back.
Matthew Bischoff
A: 

I actually spent some time looking at the code.

In InternetImage I noticed that there is this function. What you are going to have to do is get this function to inform your TableView to "refresh". You could do this by passing a delegate function to your class that is called later, but now things get real tricky. Because when you leave the view you will need to set the delegate function to "nil" for every image you are trying to download.

-(void) internetImageReady:(InternetImage*)downloadedImage {
// The image has been downloaded. Put the image into the UIImageView [imageView setImage:downloadedImage.Image]; }

Do post the answer here when you have got it, I am interested to see if this solved it or not?

John.

John Ballinger
We already implement that function, and have it call [tableView reloadData] for now, and it works great for all the cells as soon as I have scrolled. The problem, however, is that the initial on-screen cells, when they instantiate InternetImages, do not reach this method and do not report a download error. I have log statements all over this code, and for the life of me I can't figure out why NSURLConnection reports neither a success nor an error for the image download. Could it be a problem with NSURLConnection itself?
Matthew Bischoff
I just want to add that InternetImage will correctly report NSURLConnection successes and errors as soon as I have scrolled, just not initially.
Matthew Bischoff
A: 

well nice approuch? First thing check where u allocating imageview? 2nd if your image is fixed means specific than just whole code in

if(cell == nil) {

}

side that condition . I face something same problem and solved like this . let me know if this works or not.

sandy
A: 

A solution that worked for me is to move the responsibility for downloading to Object, and moving responsibility for handling the image refresh to the table cell itself. If the Object is visible on the screen and has a corresponding table cell, the image will refresh immediately once it's downloaded.

This approach would require the following changes:

  • Move the responsibility for downloading the image to the Object.
  • Modify Object to send an event/call a delegate/notify that a property has changed when the image has changed.
  • Subclass UITableViewCell to know about the Object that it will display that adds itself as a change delegate to the object when it's assigned.

Here's a sketch of how this could work:

// in the UITableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  Object *object = (Object *)[self.array objectAtIndex:indexPath.row];

  ObjectTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"objectcell"];
  if (nil == cell) {
    cell = [[[ObjectTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"objectcell"] autorelease];
  }

  cell.object = object;

  return cell;
}

// in Object

- (void) downloadImageIfNeeded {
  if (nil == image && nil == internetImage) {
     NSString *URLString = [[MyAppDelegate imageURLFromLink:(object.link) withExtension:@".jpg"]absoluteString];

     internetImage = [[InternetImage alloc] initWithUrl:URLString object:object];
     [internetImage downloadImage:self];
  }
} 

- (void) internetImageReady:(InternetImage*)downloadedImage {
  self.image = downloadedImage.image;
  self.thumbnailImage = [image _imageScaledToSize:CGSizeMake(75.0, 50.0) interpolationQuality:20];

  // notify the delegate
  [imageDelegate imageChanged: self];
}

// in ObjectTableViewCell.h
@property (nonatomic, retain) Object *object;

// in ObjectTableViewCell.m
- (Object *) object {
  return object;
}

- (void) setObject: (Object *) obj {
  if (obj != object) {
    object.imageDelegate = nil;
    [object release];
    object = nil;

    if (nil != object) {
      object = [obj retain];
      object.imageDelegate = self;

      [object downloadImageIfNeeded];
      self.textLabel.text = object.attribute;
      self.imageView.image = object.thumbnailImage;

      self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
   }
}

- (void) imageChanged: (Object *) o {
  self.imageView.image = object.thumbnailImage;
}
dmercredi
Hmm, I do like this solution, it's very thorough and thank you for the sample code. However, I am not necessarily looking to rewrite my way of loading images, I am satisfied with performance of the current way after I scroll. If there is no simpler solution then I may have to do this, but it seems like a lot of work just to get images to load on the first view.
Matthew Bischoff
+1  A: 

It indeed seems that NSUrlConnections don't retrieve data as long as the table is scrolling. Once scrolling stops, it instantly continues to retrieve data. This behavior can however be seen in any iPhone application (images aren't loaded as long as the table is scrolling) and in the end, I decided to live with that in my application, too. I think, Apple did this on purpose. Maybe to prevent the device from hammering the server with tons of image requests if the user is scrolling very fast; or maybe even to keep scrolling smooth for the user at any time.

It'd however be nice if it would at least work while scrolling slowly and only block downloading while scrolling fast.

Andreas
I understand this, but the problem is that the NSURLConnection wrapper class is not receiving data BEFORE scrolling. Once I have scrolled anywhere, image loading works properly from that point forward.
Matthew Bischoff
You can definitely run NSURLConnections while scrolling, I do it all the time. In 2.0 (and 2.1 if I recall correctly) you could not update a cell while it was moving because UITableView took a snapshot of it before it scrolled and used that in order to increase the frame rate of scrolling, but you definitely can now.
Louis Gerbarg
A: 

Okay, first off, what you have posted here is not what you are using, or you have made modifications to InternetImage you have not mentioned, since InternetImage does not respond to initWithUrl:object:. You also have not included the code to the delegate method internetImageReady:. I understand that it is not being called when something goes wrong, but it is called once you start scrolling, and having the contrast of what happens in the correct case will help people diagnose your issue.

Also, it is not entirely clear from your description what happens when you scroll. Clearly subsequent calls work, but do all the previously attempted connections suddenly start at that point, or are they completely lost?

As for what is going on, well for the reasons above I don't believe anyone can do more than give you some guesses. If I had to wager guess that if what I said in the last paragraph is happening then your issue is that (for whatever reason) the main threads runloop is not processing the NSURLConnections events until something primes the pump. This could be for a number of reasons, for instance the runloop being in the wrong mode. You could try running the NSRunLoop manually, or changing to InternetImage to explicitly set runloop, or manually starting the download.

Two quick asides

  1. InternetImage appears to do a lot of things that are idiomatically inconsistent with general Cocoa programming conventions. At least for me that makes it harder to read quickly and I am not entirely sure it is doing things correctly.
  2. Don't use _imageScaledToSize:. That is a private Apple method and if they notice that you are using it that may result in a rejection.
Louis Gerbarg
We made one single modification to InternetImage to store an object that has no bearing on the rest of the code, and when removed the same problem still persists.the code for internetImageReady doesn't matter if it is never being called, and none of the NSURLConnection delegate methods present in InternetImage are being called. What happens in the correct case is that everything works perfectly, the image is downloaded and shows up in the reloaded tableView.So how would I know exactly what needs to "prime the pump?" I will look into manually getting the runloop and report back.
Matthew Bischoff
With the asides - InternetImage is fine, it is simply a wrapper class for NSURLConnection and does nothing any differently from manually creating one and running it, other than providing convenience. Also, we have already changed the _imageScaledToSize: code and never intended to keep it, we were more concerned with getting this issue fixed. Thanks for your time on this answer tho.
Matthew Bischoff
A: 

I don't have an answer, but I can tell you that I am seeing the same sort of problem.

I am using a slightly modified version of Mark Johnson's AsyncImageView class. The asynchronous requests for the images don't cause any delegates to be called, till I scroll the tableview. As soon as I scroll a cell out of view and back, the call fires again and gets the data properly.

I read elsewhere that NSURLConnection depends on the run loop to be in a certain mode - I don't know if that figures in a situation where a URLConnection request is started in tableView:cellForIndexPath.

How would one go about debugging NSURLConnection internals?

[UPDATE]

Okay, so I solved my particular instance of this problem by starting the connection on the main thread explicitly. I put the connection setup code in a different selector, and invoked that via performSelectorOnMainThread, and everything was good again.

You should try logging the current runloop mode in the selector that starts the connection - I discovered that is was not the default runloop mode, which NSUrlConnection needs.

MostlyYes
A: 

So, there is no function to know when a CELL appears when user scrolls? (Like Flex framework does)

eBuildy