0

In one of my view controllers I have a tableview with custom tableviewcells. Each cell is associated with a unique audio file. Each cell also has a progress bar. When I tap the cells to play the audio file and scroll down, I notice that the progress bar of the reused cell is also updating. I created a custom NSObject class called OSTablePlayerController that contains the AVAudioPlayer and its functionality. However, I set the NSTimer within my custom tableviewcell class. When I tap a cell to play the following method gets called within my custom tableviewcell class:

-(void) rowSelected2: (NSURL*) url
{
    self.audioPlayer = [OSTablePlayerController getInstance];
    NSData *data=[NSData dataWithContentsOfURL:url];
    [self.audioPlayer playAudio:data];
    self.audioPlayer.isPlayerPlaying = YES;
    self.timer = [NSTimer timerWithTimeInterval:0.01 target:self selector:@selector(updateProgress:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];  
}  

- (void)updateProgress:(NSTimer *)timer 
{

    NSTimeInterval playTime = [self.audioPlayer currentTime];
    NSTimeInterval duration = [self.audioPlayer duration];

    float progress =  playTime/duration;

    [self.waveView setProgress:progress];

    if(!(self.audioPlayer.player.playing))
    {
        [self audioPlayerDidFinishPlaying:self.audioPlayer.player successfully:YES];

        progress = 0.00;

        [self.waveView setProgress:progress];

    }
}

-(void) audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
    if(flag)
    {
        [self.timer invalidate];
        self.audioPlayer.isPlayerPlaying = NO;
    }
}

The only other functionality I have implemented is pause, so that when a cell is tapped while it is already playing, the player would get paused. Tapping it again would start the player from where it was paused.

Anyone have recommendations on how to go about doing this the right way? I expect this to get pretty messy.

EDIT: Attempting to incorporate delegate methods.

//Custom tableview cell class .h
@class OSAudioTableCell;
@protocol progressDelegate <NSObject>

-(float) trackProgress;

@end

@interface OSAudioTableCell : UITableViewCell <AVAudioPlayerDelegate>
{

}

@property (nonatomic, weak) id<progressDelegate> delegate;   

//Custom table view cell .m file
-(void) rowSelected2
{
        [self.waveView setProgress:[self.delegate trackProgress]];
}

-(void) prepareForReuse
{
    [super prepareForReuse];
    [self.timer invalidate];
    self.waveView.progress = 0;    
}

//view controller with tableview 
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
 {
             [((OSAudioTableCell*)[self.audioTable cellForRowAtIndexPath:indexPath]) rowSelected2];
             self.audioPlayer = [[OSTablePlayerController alloc] init];
             [self.audioPlayer playAudio:data];
             self.audioPlayer.isPlayerPlaying = YES;
             self.audioPlayer.playerTimer =[NSTimer timerWithTimeInterval:0.01 target:self selector:@selector(trackProgress:) userInfo:nil repeats:YES];
             [[NSRunLoop mainRunLoop] addTimer:self.audioPlayer.playerTimer forMode:NSRunLoopCommonModes];
}

-(float) trackProgress
{
    NSTimeInterval playTime = [self.audioPlayer currentTime];
    NSTimeInterval duration = [self.audioPlayer duration];

    float progress =  playTime/duration;

    if(!(self.audioPlayer.player.playing))
    {
        [self audioPlayerDidFinishPlaying:self.audioPlayer.player successfully:YES];

        progress = 0.00;
    }

    return progress;
 }
Brosef
  • 2,945
  • 6
  • 34
  • 69
  • Are you trying to solve a specific problem? Or just looking for general architectural advice? – Aaron Brager Aug 03 '14 at 18:57
  • Well, I guess the specific problem would be to prevent the reused cells progress bar from updating. If thats too much to ask, then general architectural advice would be appreciated. – Brosef Aug 03 '14 at 19:01

2 Answers2

0

Use a property to track which cell is currently selected and playing the audio.

Use a property say selectedIndexPath.When you tap on cell assign this property with current tapped indexpath.In cellForRowAtIndexPath compare selectedIndexPath with the current cell returning if selectedIndexPath match than show progress bar else hide for all which do not match.

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

    // your other code
   if([selectedIndexPath isEqual:indexPath]){

      //allocate progressbar and start timer

   }
   else{
     //deallocate progressbar and invalidate timer
   }

}

You can check in didSelectRowAtIndexPath whether selectedIndexPath is equal to current tap cell if so than check for audioPlayer playing status if it is playing than pause it else start it again

codester
  • 36,891
  • 10
  • 74
  • 72
  • It might be simpler to simply reset the progress view and disable the timer in `prepareForReuse` in the cell subclass. – Aaron Brager Aug 03 '14 at 19:04
  • @AaronBrager that will definitely reset the progress bar. But the problem with that is when I scroll back up to the cell that is playing and the progress is zero. Remember, the audio player doesn't stop when I scroll down. – Brosef Aug 03 '14 at 19:05
  • @AaronBrager but than again it needs to track for which cell it needs to start again – codester Aug 03 '14 at 19:06
  • @codester I'd prefer not to hide the progress bar. Its design is critical to the structure of the tableview cell and must be visible at all times. – Brosef Aug 03 '14 at 19:09
  • @Brosef you are hiding this for other cells which are not selected and not visible.Hiding will also not need to reset your timer and start again.So it is better solution.When your cell again become visible it shows the progress bar – codester Aug 03 '14 at 19:11
  • @codester I absolutely cannot hide the progress bar. Trust me on that one. – Brosef Aug 03 '14 at 19:24
  • @Brosef so do not hide it.First stop the timer and than asssign nil to all progressbar if cells are not selected and visibles.When you comeback to selected cell allocate the progressbar again – codester Aug 03 '14 at 19:33
  • @codester in prepareForReuse I can invalidate timer and set progress to zero, but when I scroll back to selected cell that is playing, progress will be zero but audio will still be playing. How would I set timer and progress back to match the player. – Brosef Aug 03 '14 at 19:43
  • As i said you need to track the `selectedIndexPath` for current cell which is selected and playing audio.So you need to make a property in your `viewController` as `selectedIndexPath` and make method in `cellclass` to start a timer and progress.When your `selectedIndexPath` equals to `cellForRowAtIndexPath` than call cell method to start timer and progress.Look for answer code.Let me know if you find any issues – codester Aug 03 '14 at 19:47
  • @codester ok say I have 1 min audio file playing. When the file reaches 30 seconds and I scroll down I have to invalidate the timer and set progress to zero because cell will be reused, but when I scroll back up how to I set the timer and progress back to 30sec? – Brosef Aug 03 '14 at 20:21
  • @Brosef I have seen your code you are using 0.01 seconds and repeating it in every 0.01 so i think there is no need to calculate 30 seconds .Just invalidate and restart it and stop the timer in `audioPlayerDidFinishPlaying` and set `selectedIndexPath` to nil – codester Aug 03 '14 at 20:28
0

In your UITableViewCell subclass, you should implement prepareForReuse. In this method, return your cell to its default state (exactly as it is after the first time it's initialized).

Then, in cellForRowAtIndexPath: every time you dequeue a cell it will be fresh. Just configure it as appropriate for the item at the current index path. If that's the item that playing, update and start the progress bar. If it's not, then don't.

I think your general problem is that each cell is maintaining its own timer and audio player, but there should only be one of these (in your controller, not in your cell subclass). Your cell is a view, and should therefore be as stupid as possible. It shouldn't have any idea what it's displaying, or what happens when it's tapped. It should just communicate to the controller when it's tapped, and the controller should do the rest.

Aaron Brager
  • 65,323
  • 19
  • 161
  • 287
  • I agree with this. I'll have to move the `NSTimer` to my OSTablePlayerController class. How would I communicate the timer with the cell to make sure the progress bar is only updating for one cell? – Brosef Aug 03 '14 at 19:23
  • Make a delegate protocol for your cell with a method like `someButtonWasTappedOnCell:`. Make the view controller the cell's delegate. Have the cell call this method on the delegate. Then in your view controller's implementation of `someButtonWasTappedOnCell:`, you can call `[self.timer doWhatever]`. – Aaron Brager Aug 03 '14 at 19:25
  • [Sample code here](http://stackoverflow.com/a/12660523/1445366) if you haven't done it before. – Aaron Brager Aug 03 '14 at 19:26
  • @AaronBrager and @Brosef i think he have timer in class `rowSelected2`.So chek if `self.timer` is there than invalidate it and if not than start it only for selected cell. – codester Aug 03 '14 at 19:28
  • @AaronBrager I edited in your suggestions. One catch: for `didSelectRowAtIndexPath` I'm trying to pass in my delegate method for the selector parameter for `timerWithTimeInterval` but it won't recognize. Any suggestions? – Brosef Aug 03 '14 at 21:41