Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom UISlider like the Music app

I've built a custom slider to show the progress of a music track playing and to allow scrubbing within the track.

Both function fine, but there is a slight lag (and jumpy movement) once dragging has stopped and the slider is repositioned - The Apple Music app slider is seamless.

_scrubberSlider = [[ScrubberSlider alloc] initWithFrame:CGRectMake(0, 0, 300, 30)];
_scrubberSlider.continuous = YES;
_scrubberSlider.maximumValue = 1.0f;
_scrubberSlider.minimumValue = 0.0f;
 [_scrubberSlider addTarget:self action:@selector(handleSliderMove:) forControlEvents:UIControlEventValueChanged];

- (void) handleSliderMove:(UISlider*)sender
{
    CGFloat currentPlayback = sender.value * [[_theMusicPlayer.nowPlayingItem valueForKey:MPMediaItemPropertyPlaybackDuration] floatValue];
    _theMusicPlayer.currentPlaybackTime = currentPlayback;
}

-(void) handleTrackTime
{
    if (!trackTimer)
    {
    NSNumber *playBackTime = [NSNumber numberWithFloat: _musicPlayer.currentPlaybackTime];
    trackTimer = [NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(timeIntervalFinished:) userInfo:@{@"playbackTime" : playBackTime} repeats:YES];
    }
}

-(void) timeIntervalFinished:(NSTimer*)sender
{
     [self playbackTimeUpdated:_musicPlayer.currentPlaybackTime];
}

-(void) playbackTimeUpdated:(CGFloat)playbackTime 
{
    // Update time label
    [self updatePosition];
}

-(void) updatePosition
{
   if (!_scrubberSlider.isScrubbing)
   {
   CGFloat percent = _theMusicPlayer.currentPlaybackTime / [[_theMusicPlayer.nowPlayingItem valueForKey:MPMediaItemPropertyPlaybackDuration] floatValue];
   _scrubberSlider.value = percent;
   }
}

The custom Slider

@interface ScrubberSlider : UISlider

@property(nonatomic) BOOL isScrubbing;

@end

@implementation ScrubberSlider

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
    }
    return self;
}

-(BOOL) beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    [super beginTrackingWithTouch:touch withEvent:event];

    _isScrubbing = YES;

    return YES;
}

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    [super endTrackingWithTouch:touch withEvent:event];

    _isScrubbing = NO;
}

@end
like image 842
daihovey Avatar asked Feb 28 '13 07:02

daihovey


1 Answers

Perhaps you're going about this the wrong way. Instead of trying to be exactly synced to the time of the track (which leads to some pretty nasty race conditions, and is precisely why your slider doesn't update properly when "thrown" rather than dragged, held, then released), try to use the timer you have as a "tick". You can decrease the rate of fire down to, say, once every half a second, then use basically the same logic you have now, but instead of trying to get an exact fractional value in -updatePosition, you "tick" the slider forward.

-(void) updatePosition
{
   if (!_scrubberSlider.isScrubbing)
   {
       _scrubberSlider.value += .5f;
   }
}

It's not perfect, but then again, it's far more efficient and seamless than trying to sync yourself to the timing of a music track with NSTimer (which can be disgustingly inaccurate for precise movements). This also requires that your slider's max value be equivalent to the track's max value, so an example method to start playback would include something like:

- (IBAction)startPlayback:(id)sender {
    //... Handle however you wish to play the track
    _scrubberSlider.maximumValue = [[_theMusicPlayer.nowPlayingItem valueForKey:MPMediaItemPropertyPlaybackDuration] floatValue];
    [self handleTrackTime];
}
like image 187
CodaFi Avatar answered Oct 22 '22 10:10

CodaFi