Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

Dookieman

macrumors 6502
Original poster
Oct 12, 2009
390
67
First, I'm working on an app that downloads audio files and I wrote a DownloadManager class to do this. I'm using a UIProgressView inside of UITableViewCell to show the progress of the download. I got everything working, I can get it to appear and move based on the progress of the download as long I don't scroll the cell off screen, but when I scroll the cell off the screen, it disappears of course. This is due to cellForRowAtIndexPath not knowing it needs to display a progressView when it's downloading since it's automatically set to hidden unless it's in the process of downloading.

So I did a quick Core Data save in my DownloadManager class, "episodeDownload = @Yes" to ensure the progressView shows if the episode is downloading. This works, I can get it to appear, but the current progress isn't displaying anymore... any idea on how to always the current progress if the progressView goes from hidden to showing?

Second, I want a persistent UIToolBar at the bottom of the screen. Is there a way I can use the one built into UINavigationController to accomplish this? I did a quick test and the BarButtonItems I set in the initial view controller disappear when I change views.
 
Progress indicators in table view cells are updated with KVO. You need a data object that represents the download that has a progress @Property. You assign the downloadJob to your table view cell in cellForRowAtIndexPath. When it's assigned inside the cell class you addObserver to the progress @Property and removeObserver from the previous downloadJob. In observeValueForKeyPath: you update the progress indicator.

For the toolbar each view controller controls whether the toolbar is visible by setting the toolbarItems.
 
Progress indicators in table view cells are updated with KVO. You need a data object that represents the download that has a progress @Property. You assign the downloadJob to your table view cell in cellForRowAtIndexPath. When it's assigned inside the cell class you addObserver to the progress @Property and removeObserver from the previous downloadJob. In observeValueForKeyPath: you update the progress indicator.

For the toolbar each view controller controls whether the toolbar is visible by setting the toolbarItems.
Sounds good for the UIToolBar.

Do you have an example for the UIProgressView?
 
This is not a complete implementation but this is the guts of it. In cellForRowAtIndexPath setData to the appropriate Data object and then during the download set the progress value on the Data object and the table view cell will update its progress view.

Code:
@interface Data
@property double progress
@end

@interface MyTableViewCell
@property Data* data;
@property (weak, nonatomic) IBOutlet UIProgressView* progressView;
@end

@implementation MyTableViewCell

-(void)setData:(Data*)data
{
     if (data == self.data)
          return;

     [self.data removeObserver:self forKeyPath:@"progress"];
     _data = data;
     [self.data addObserver:self forKeyPath:@"progress"];
}

-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
     if ([keyPath isEqualToString:@"progress"]) {
          self.progressView.progress = [object progress];
     }
}

@end
 
This is not a complete implementation but this is the guts of it. In cellForRowAtIndexPath setData to the appropriate Data object and then during the download set the progress value on the Data object and the table view cell will update its progress view.

Code:
@interface Data
@property double progress
@end

@interface MyTableViewCell
@property Data* data;
@property (weak, nonatomic) IBOutlet UIProgressView* progressView;
@end

@implementation MyTableViewCell

-(void)setData:(Data*)data
{
     if (data == self.data)
          return;

     [self.data removeObserver:self forKeyPath:@"progress"];
     _data = data;
     [self.data addObserver:self forKeyPath:@"progress"];
}

-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
     if ([keyPath isEqualToString:@"progress"]) {
          self.progressView.progress = [object progress];
     }
}

@end


So I'm still having trouble wrapping my head around this. Here is the code from the UITableViewCell.

Code:
- (IBAction)optionsButton:(id)sender {
    NSNumber *indexRow = [NSNumber numberWithInteger:self.moreInfoButton.tag];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"DownloadEpisode" object:indexRow];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateProgressView:currentSize:) name:@"UpdateProgressBar" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setProgressViewMaxValue:) name:@"SetProgressMaxValue" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showProgressView) name:@"ShowProgressView" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hideProgressView) name:@"HideProgressView" object:nil];
    NSLog(@"Firing! %@", indexRow);
}


-(void)setProgressViewMaxValue:(NSNotification *)notification {
    self.totalBytes = [[notification object] longLongValue];
}


- (void)updateProgressView:(NSNotification *)notification currentSize:(int64_t)currentSize {
 
    currentSize = [[notification object] longLongValue];
    if (self.progressView.progress != self.totalBytes) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.progressView setProgress:((double)currentSize/(double)self.totalBytes) animated:YES];
        });

    }

}

And here is the code from my Download Manager Class which contains the Network code.

Code:
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
 
    [[NSNotificationCenter defaultCenter] postNotificationName:@"UpdateProgressBar" object:[NSNumber numberWithLongLong:totalBytesWritten]];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"SetProgressMaxValue" object:[NSNumber numberWithLongLong:totalBytesExpectedToWrite]];
}

This works, but does not appear correctly once the cell has been moved off screen... Here is my cellForRowAtIndexPath

Code:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   
    TitleCell *titleCell = (TitleCell *)[tableView dequeueReusableCellWithIdentifier:@"titleCell"];
   
    if (titleCell == nil) {
        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"TitleCell" owner:self options:nil];
        titleCell = [nib objectAtIndex:0];
    }
   
    EpisodeCell *episodeCell = (EpisodeCell *)[tableView dequeueReusableCellWithIdentifier:@"episodeCell"];
   
    if (episodeCell == nil) {
        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"EpisodeCell" owner:self options:nil];
        episodeCell = [nib objectAtIndex:0];
    }
   
    if (indexPath.section == 0) {
        titleCell.userInteractionEnabled = NO;
        return titleCell;
       
    } else {
       
       
       
        self.episodeModel = [self.episodeArray objectAtIndex:indexPath.row];
       
        episodeCell.episodeTitleLabel.text = self.episodeModel.episodeTitle;
        episodeCell.moreInfoButton.tag = indexPath.row;
        episodeCell.progressView.tag = indexPath.row;
       
        if ([self.episodeModel.episodeDownloading isEqual:@"YES"]) {
            episodeCell.progressView.hidden = FALSE;
            NSLog(@"Downloading!");
        } else {
            episodeCell.progressView.hidden = TRUE;
            NSLog(@"Not Downloading!");
        }
       
        return episodeCell;
       
       
    }
   
   
}
 
OK, you want to do this with notifications. That can work.

Your download code doesn't need to know anything about progress bars. It shouldn't be sending notifications to show/hide progress bars. it only knows about the progress itself. It should post a notification with the progress percent. Sending the total bytes and current bytes in separate notifications doesn't make sense.

Presumably the cause of your bug is that you register for the notifications in the optionsButton action method and this method isn't called when the cell is dequeued in cellForRowAtIndexPath. You need to make sure those addObserver calls happen from inside cellForRowAtIndexPath. (Also remember to removeObserver.)

Your code seems to assume only one download can happen at a time.
 
OK, you want to do this with notifications. That can work.

Your download code doesn't need to know anything about progress bars. It shouldn't be sending notifications to show/hide progress bars. it only knows about the progress itself. It should post a notification with the progress percent. Sending the total bytes and current bytes in separate notifications doesn't make sense.

Presumably the cause of your bug is that you register for the notifications in the optionsButton action method and this method isn't called when the cell is dequeued in cellForRowAtIndexPath. You need to make sure those addObserver calls happen from inside cellForRowAtIndexPath. (Also remember to removeObserver.)

Your code seems to assume only one download can happen at a time.

Hey sorry for the late response. I just got a chance to work on this again. Still no luck getting this to work correctly, I feel like pulling my hair out because I know I'm making this harder than it is.

How would I register the notifications for the progressView if my progressView update code is located in the CustomCell.m and not in my TableViewController? I understand why I want/should register the notification in the cellForRowAtIndexPath, but I'm not sure how I would do that since the method is in a different class.

I think I may be approaching this the wrong way as well. Should I be doing the progressView update from my TableViewController? If so, how do I do that?
 
The code I showed above was written for KVO notifications but almost the exact same thing can be accomplished with NSNotifications. (KVO is cooler.)

In your tableviewcell you want to have a method like the setData: method I showed. This is used in cellForRowAtIndexPath to set the data model object that represents the network transaction. That's where you register/unregister for the notifications (probably also in dealloc).

So when a row is shown the first time it registers for the notifications and then updates the progressView in response. When the row scrolls offscreen it may still receive the notifications but that's not a problem. If a new row scrolls onscreen it will go through cellForRowAtIndexPath and get the data model object assigned and register for the notifications and then update its progressView.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.