How to prevent periodicTimeObserver updating the progressBar while user drags the slider manually?

197 Views Asked by At

I have a periodicTimeObserver and it updates the elapsed and remaining timeLabels in the way I want, but the slider is jumping. How to prevent periodicTimeObserver updating the UISlider while user drags the slider manually?

In this GIF you can see weird acting of the slider.

This is my UISlider

private lazy var progressBar: UISlider = {
let v = UISlider()
v.translatesAutoresizingMaskIntoConstraints = false
//v.minimumTrackTintColor = UIColor(named: "PlayerColors")
v.isContinuous = false
return v
}()

Periodic time observer which updates the UISlider and the elapsed and remaining time labels.

player = AVPlayer(playerItem: playerItem)

player!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { (CMTime) -> Void in
    
if self.player!.currentItem?.status == .readyToPlay {
    
let currentTime : Float64 = CMTimeGetSeconds(self.player!.currentTime());
let totalTime : Float64 = CMTimeGetSeconds(self.player!.currentItem!.duration);

    self.progressBar.value = Float(currentTime)
    self.progressBar.minimumValue = 0
    self.progressBar.maximumValue = Float(totalTime)

 
    
self.elapsedTimeLabel.text = self.stringFromTimeInterval(interval: currentTime)
self.remainingTimeLabel.text = self.stringFromTimeIntervalRemaining(interval: totalTime - currentTime)

The function that should seek to a point of the audio and update the time labels.

@objc func progressScrubbed(_ :UISlider) {
           
       let seconds : Int64 = Int64(self.progressBar.value)
       let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
       
           
           player!.seek(to: targetTime)
           
           if player!.rate == 0
           {
               play()
           }
       }
1

There are 1 best solutions below

0
Bulat Yakupov On

You need to know if user is interacting with a slider in order to ignore PeriodicTimeObserver. Moreover you need to reset PeriodicTimeObserver on each seek. So let's create a custom UISlider and override a one method:

class MySlider: UISlider {

    var onTouchesBegan: (() -> ())?
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        
        onTouchesBegan?()
    }

}

Now you can create a parameter which will track if slider is touched or not and set it in closures of MySlider:

private var isTouchingSlider: Bool = false

private lazy var progressBar: MySlider = {
    let v = MySlider()
    v.translatesAutoresizingMaskIntoConstraints = false
    v.isContinuous = false
    
    v.onTouchesBegan = { [weak self] in
        self?.isTouchingSlider = true
    }
    
    return v
}()

And your periodic observer methods would look like this:

var periodicObserverToken: Any?

func addPeriodicTimeObserver() {
    let interval = CMTime(
        seconds: 1,
        preferredTimescale: CMTimeScale(NSEC_PER_SEC)
    )
    
    periodicObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
        guard let `self` = self, let player = self.player, player.currentItem?.status == .readyToPlay, let currentItem = self.player.currentItem, !self.isTouchingSlider else { return }
        
        let currentTime : Float64 = CMTimeGetSeconds(player.currentTime());
        let totalTime : Float64 = CMTimeGetSeconds(currentItem.duration);
        
        self.progressBar.value = Float(currentTime)
        self.progressBar.minimumValue = 0
        self.progressBar.maximumValue = Float(totalTime)
    }
}

private func removePeriodicTimeObserver() {
    guard let periodicObserverToken = periodicObserverToken else { return }
    player?.removeTimeObserver(periodicObserverToken)
    self.playerPeriodicTimeObserver = nil
}

You need to make all the necessary updates when slider is updated:

@objc func progressScrubbed(_ :UISlider) {
    let seconds : Int64 = Int64(self.progressBar.value)
    let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
    
    removePeriodicTimeObserver()
    isTouchingSlider = false
    player!.seek(to: targetTime)
    addPeriodicTimeObserver()
    
    if player!.rate == 0 {
        play()
    }
}