1

I am downloading images asynchronously and displaying them in a UITableView. While theimage is downloading, a UIProgressView should be displayed in the corresponding table row. After the download is complete, progress view should be replaced by the actual image.

In my table view, I am using a custom cell called ProgressTableViewCell subclassed from UITableViewCell. It has a UIProgressView IBOutlet.

I have created an NSOperation from NSURLConnection and added them to an NSOperationQueue. As the delegate's

didReceiveData

method is called, a notification is posted to my table view controller to update the corresponding table row with

reloadRowsAtIndexPaths

method of table view. My cellForRowAtIndexPath does the following for the reloaded row:

   ProgressTableViewCell *cell = (ProgressTableViewCell*)[tableView dequeueReusableCellWithIdentifier:@"ProgressCell"];

    float received = [[downloadInfo objectForKey:@"receivedBytes"] floatValue];
    float total = [[downloadInfo objectForKey:@"totalFileSize"] floatValue];

    NSNumber* percentage= [NSNumber numberWithFloat:received/total];
    NSMutableDictionary* userInfo = [[NSMutableDictionary alloc] init];
    NSLog(@"percentage %f", percentage.floatValue);
    [userInfo setObject:cell forKey:@"cell"]; 
    [userInfo setObject:percentage forKey:@"percentage"];  

    [self performSelectorOnMainThread:@selector(updateProgressView:) withObject:userInfo waitUntilDone:NO];
    NSLog(@"received: %@", [downloadInfo objectForKey:@"receivedBytes"]);

    NSLog(@"Progress: %f", cell.progressView.progress);
    return cell;

The updateProgressView method looks like

- (void)updateProgressView :(NSMutableDictionary *)userInfo
{
    ProgressTableViewCell* cell = [userInfo valueForKey:@"cell"];

    NSNumber* progress = [userInfo valueForKey:@"percentage"];

   [cell.progressView setProgress:progress.floatValue ];
    NSLog(@"Progress after update: %f", cell.progressView.progress);
}

I am updating the progress view on the main thread and I have even tried setting waitUntilDone to YES but to no avail. My progress view stays at the zero point. Occasionally when I am debugging I can see some change in the progress indicator which makes me think it might be a timing problem. But how to solve it?

EDIT: Here is NSURLConnection delegate's didReceiveData method:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [_responseData appendData:data];
    NSNumber* bytes = [NSNumber numberWithUnsignedInt:[data length]];

    NSLog(@"received bytes:%d", [bytes intValue] );
    NSMutableDictionary* userInfo = [[NSMutableDictionary alloc] init];
    [userInfo setObject:_responseId forKey:@"responseId"];  
    [userInfo setObject:bytes forKey:@"receivedBytes"];

    [self fireNotification: [NSNotification
                                   notificationWithName:@"DidReceiveData"
                                   object:self userInfo:userInfo]];
}



- (void)fireNotification :(NSNotification *)aNotification
{
    [[NSNotificationCenter defaultCenter] postNotification:aNotification];
}

And here is my view controller's method that gets the notification:

-(void) dataReceived:(NSNotification *)notification {

    NSNumber* responseId = [[notification userInfo] objectForKey:@"responseId"];
    NSNumber*  bytes = [[notification userInfo] objectForKey:@"receivedBytes"];

    NSMutableDictionary* downloadInfo = [self getConnectionInfoForId:responseId];

    NSLog(@"received bytes:%ld for response %@", [bytes longValue], responseId );
    NSNumber* totalBytes = [NSNumber numberWithInt:([bytes longValue] + [[downloadInfo objectForKey:@"receivedBytes"] longValue]) ];
    [downloadInfo setObject:totalBytes forKey:@"receivedBytes"];

    float received = [[downloadInfo objectForKey:@"receivedBytes"] floatValue];
    float total = [[downloadInfo objectForKey:@"totalFileSize"] floatValue];

    [downloadInfo setObject:[NSNumber numberWithFloat:received/total] forKey:@"progress"];

    [self reloadRowForResponse:responseId];

}

I have also added a nil check to my cellForRowAtIndexpath method as recommended:

ProgressTableViewCell *cell = (ProgressTableViewCell*)[tableView dequeueReusableCellWithIdentifier:@"ProgressCell"];
    if (cell == nil)
    {
        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"ProgressCell" owner:self options:nil];
        cell = [nib objectAtIndex:0];
    }
    float received = [[downloadInfo objectForKey:@"receivedBytes"] floatValue];
    float total = [[downloadInfo objectForKey:@"totalFileSize"] floatValue];

    NSNumber* percentage= [NSNumber numberWithFloat:received/total];
    NSMutableDictionary* userInfo = [[NSMutableDictionary alloc] init];
    NSLog(@"cell:%@", cell);
    NSLog(@"percentage %f", percentage.floatValue);
    [userInfo setObject:cell forKey:@"cell"];  
    [userInfo setObject:percentage forKey:@"percentage"]; 

    [self performSelectorOnMainThread:@selector(updateProgressView:) withObject:userInfo waitUntilDone:NO];

    return cell;
ganime
  • 129
  • 2
  • 9
  • Can you post your delegate method and the method you're using to handle the notification? My guess is if you've got the index path and the download info available in the notification handler, you should be updating the progress view there, rather than reloading the table view every time. – Joe Hankin Nov 29 '12 at 22:02
  • I have posted those method Joe. You can see them above under EDIT. The thing is I have a progress view for each image that is downloading, that is why I am doing my updates within the table view's datasource delegate method. – ganime Nov 30 '12 at 11:57

3 Answers3

3

I think you're taking the wrong approach by reloading the table cell every time the delegate method gets called. You can instead just grab the visible cell and update the progress indicator directly, rather than going through the data source.

I'm assuming you have some way of converting responseId to the index path of the row you want to update -- let's say that's called indexPathForResponseID: in your controller. Rather than reloading the cell, you can just grab the cell if it's visible and update its progress indicator:

- (void)dataReceived:(NSNotification *)notification {

    ...

    float received = [[downloadInfo objectForKey:@"receivedBytes"] floatValue];
    float total = [[downloadInfo objectForKey:@"totalFileSize"] floatValue];

    NSIndexPath *cellPath = [self indexPathForResponseID:responseId];
    ProgressTableViewCell *cell = (ProgressTableviewCell *)[self.tableView cellForRowAtIndexPath:cellPath];
    if (cell) {
        // make sure you're on the main thread to update UI
        dispatch_async(dispatch_get_main_queue(), ^{ 
            [cell.progressView setProgress: (received / total)];
        }
    }
}

You should be aware, though, that this solution won't suffice if you have more downloads than visible cells -- you should also be storing the progress of each download somewhere in your data source so that if the table view DOES need to reload a cell (due to a scroll), it knows how to set the progress indicator.

Joe Hankin
  • 950
  • 8
  • 15
  • At last have found a solution (See my own answer). But I did try your suggestion today and it seems to work. Can't remember why I couldn't get it to work 2 months ago. I have gone for another way of doing it because I would have to do more work for the reasons you have mentioned. Thanks anyway. – ganime Jan 19 '13 at 22:29
0

I've found in many cases like this that the cell you think you have isn't the one that's updating. When you reload, your code is popping a cell off the reusable, which is basically an old cell. If you reload, that cell gets replaced by another. (You also haven't included allocating a new cell if the reusable ones return nil) If your cell scrolls off, it gets reloaded, so you have to make sure that you're putting the progress bar on the cell that's there, not the one that used to be there. You might want to NSLog the address of the cell you started the transfer with, and then again each time you update the progress. You need to update the progress bar with the current cell at that index path. It may not be the one you initially started the process with.

Owen Hartnett
  • 5,925
  • 2
  • 19
  • 35
  • Indeed Owen. When I NSLog the cell address, I get a different one each time. But why is this happening even with reusing cells? Also, when I only have one image downloading, how is it that a different progres view is being updated. How can I "make sure that I am putting the progress bar on the cell that's there, not the one that used to be there". ? – ganime Nov 30 '12 at 12:00
  • 1
    When you reuse cells, that's what you're doing. The cell that used to be at row zero gets reused in row 12 when you scroll. So, the cell object that was used to initiate the download is not in the same place. I use a custom cell object so that I can determine which cell has my downloading object, and search through them to find it. Also note that your downloading cell may be scrolled offscreen. What's important that when you do load/reuse a new cell, you update the references for any file downloads. – Owen Hartnett Nov 30 '12 at 19:15
0

Coming back to this almost 2 months later, I seem to find a solution. Not sure if this is good practice but creating a new UIProgressView each time the progress is updated seems to solve my problem. Here is the method:

-(void)updateProgressView :(NSMutableDictionary *)userInfo
{
    ProgressTableViewCell* cell = [userInfo objectForKey:@"cell"];
    cell.backgroundColor =[UIColor darkGrayColor];
    NSNumber* progress = [userInfo objectForKey:@"percentage"];
    NSLog(@"Progress before update: %f", cell.progressView.progress);

    UIProgressView *pView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
    pView.frame = CGRectMake(150, 200, 150, 9);
    pView.progress = progress.floatValue;

    [cell.contentView addSubview:pView];
}

Many thanks to everyone for their help.

ganime
  • 129
  • 2
  • 9