Search code examples
iosprogress-baruitableviewreuseidentifier

Cell's progress bar duplicates on other cells


I have a cell reuse problem.

All my cells are identical, there is only one section. Cells include a label, button and progress bar.

The button triggers a sound (it's a simple "play" button). When the AVAudioPlayers delegate method is called, audioPlayerDidFinishPlaying:, I nullify the NSTimer that updates the progress bar, hide the progress bar, and nullify the audio player as well.

All this happens in my cell's class which inherites from UITableViewCell.

Everything works like it should, except for one thing :

Scrolling while playing a sound shows the same progress bar on the new cells.

So if I tap, say, the second cell, the second next cell being shown when scrolling will have a progress bar moving forward (and stop+hide when the sound finishes).

I'll show you my custom cell complete class, I hope this will help. I don't really know how to fix this. I tried if-statements in the cellForRow: but without any real success.

@implementation SoundItemTableViewCell

- (void)awakeFromNib {
    // Initialization code
    self.progressBar.hidden = YES;
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];

    // Configure the view for the selected state
}

- (IBAction)playSound:(id)sender {

    [self.progressBar setProgress:0];
    self.progressBar.hidden = NO;

    NSString *filename = self.fileName;
    [self playSoundWithURL:[NSURL URLWithString:filename]];
}

- (void)playSoundWithURL:(NSURL*)url{


    NSError *error;
    if (self.audioPlayer){

        if(self.audioPlayer.playing){
            [self.audioPlayer stop];
        }
    }

    self.audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
    self.audioPlayer.delegate = self;
    self.tmrProgress = [NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(updateElapsedTime) userInfo:nil repeats:YES];

    [self.audioPlayer prepareToPlay];
    [self.audioPlayer play];
    NSLog(@"Starting sound play with item : %@",url);

}

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{

    [self.progressBar setProgress:0 animated:YES];
    self.progressBar.hidden = YES;
    //self.progressBar = nil;
    [self.tmrProgress invalidate];
    self.tmrProgress = nil;
    player = nil;
    self.audioPlayer = nil;
    NSLog(@"Audio Finish");
}


- (void)updateElapsedTime{
    if (self.audioPlayer) {
        NSLog(@"Updating progress");
        [self.progressBar setProgress:[self.audioPlayer currentTime] / [self.audioPlayer duration]];
    }
}

The tableview. pushList is an array containing file URLs

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    static NSString *identifier = @"SoundItemTableViewCell";
    SoundItemTableViewCell *cell = [self.tbPushList dequeueReusableCellWithIdentifier:identifier];

    if (cell == nil) {
        cell = [[SoundItemTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    cell.fileName    = [pushList objectAtIndex:indexPath.row];
    cell.lbName.text = [[[pushList objectAtIndex:indexPath.row] lastPathComponent] stringByDeletingPathExtension];
    return cell;

}

The best I could achieve was to hide the progress bar when it goes out of view, and therefore no other progress bar would show either, but rescrolling to my playing cell would then have a sound playing with no progress bar, which is unsatisfactory/bad UX behaviour.


Solution

  • The problem is you're reusing your audioplayer and surrounding logic. You need something external to check whether the cell in question is the one playing the sound and you need to move the controller logic into the controller.

    Add this property to your view controller...

    @property (nonatomic) NSString *playingFilename;

    Move - (IBAction)playSound:(id)sender into the view controller and set the string when you select a specific button...

    - (IBAction)playSound:(id)sender {
    
        UIButton *playButton = sender;
        NSInteger index = playButton.tag;
    
        SoundItemTableViewCell *cell = (SoundItemTableViewCell *)[self.tbPushList cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
    
        [cell.progressBar setProgress:0];
        cell.progressBar.hidden = NO;
    
        self.playingFilename = [pushList objectAtIndex:index];
    
        // Personally I'd move the playing logic into the controller as well... spin up as many audio players as you need... but they shouldn't be in the cell.
        [self playSoundWithURL:[NSURL URLWithString:self.playingFileName]];
    }
    

    then

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
        static NSString *identifier = @"SoundItemTableViewCell";
        SoundItemTableViewCell *cell = [self.tbPushList dequeueReusableCellWithIdentifier:identifier];
    
        if (cell == nil) {
            cell = [[SoundItemTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
        }
    
    
       // tag your button.....
        cell.playButton.tag = indexPath.row;
    
        cell.lbName.text = [[[pushList objectAtIndex:indexPath.row] lastPathComponent] stringByDeletingPathExtension];
    
        // check to see if this index contains the right file
       BOOL isRightFile = ([self.playingFilename isEqualToString:[pushList objectAtIndex:indexPath.row]])
    
        cell.progressBar.hidden = !isRightFile;
    
        return cell;
    }