darkginger
darkginger

Reputation: 690

Make UISlider Update with Progress Using AVAudioPlayer

I'm making a simple audio player app, and it allows a user to change where they are in a track using the UISlider.

What it doesn't do: the slider doesn't update based on the track's progress. I found a similar question here, and the second answer was responsive to AVAudioPlayer -- suggesting using currentTime to update the slider. I want to try to do that.

Here's my code:

import UIKit
import AVFoundation

class MusicViewController: UIViewController {

    ....

    @IBOutlet var Slider: UISlider!

    @IBAction func changeAudioTime(_ sender: Any) {
        Slider.maximumValue = Float(audioPlayer.duration)
        audioPlayer.stop()
        audioPlayer.currentTime = TimeInterval(Slider.value)
        audioPlayer.prepareToPlay()
        audioPlayer.play()
    }

    func updateSlider(){

        Slider.value = Float(audioPlayer.currentTime)
        print("Changing works")
    }

The first function works fine -- the user can change where they are. However, the second function to me should change the Slider.value based on the currentTime of the AudioPlayer. It doesn't do that.

It's also not printing, so my belief is it isn't firing.

I'm new to Swift so I am likely missing something. Any ideas?

Upvotes: 3

Views: 5129

Answers (4)

Sreekuttan
Sreekuttan

Reputation: 1964

Apple's suggested solution is adding a periodicTimeObserver to the AVPlayer object.

Code:

private(set) var duration: TimeInterval = 0.0
private(set) var currentTime: TimeInterval = 0.0

private let player = AVPlayer()
private var timeObserver: Any?


/// Adds an observer of the player timing.
private func addPeriodicTimeObserver() {
    // Create a 0.5 second interval time.
    let interval = CMTime(value: 1, timescale: 2)
    timeObserver = player.addPeriodicTimeObserver(forInterval: interval,
                                                  queue: .main) { [weak self] time in
        guard let self else { return }
        // Update the currentTime and duration values.
        // Update your slider or time-elapsed label values
        currentTime = time.seconds
        duration = player.currentItem?.duration.seconds ?? 0.0
    }
}


/// Removes the time observer from the player.
private func removePeriodicTimeObserver() {
    guard let timeObserver else { return }
    player.removeTimeObserver(timeObserver)
    self.timeObserver = nil
}

If you want to read more refer to original doc by Apple

Upvotes: 0

Artem Garmash
Artem Garmash

Reputation: 164

You need to set up a CADisplayLink and update the position of the slider on each invocation of its selector. Here's how the whole AVAudioPlayer-UISlider binding can be done:

// 1
@IBOutlet weak var slider: UISlider! {
    didSet {
        slider.isContinuous = false

        slider.addTarget(self, action: #selector(didBeginDraggingSlider), for: .touchDown)
        slider.addTarget(self, action: #selector(didEndDraggingSlider), for: .valueChanged)
    }
}

// 2
var player: AVAudioPlayer

// 3
lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: #selector(updatePlaybackStatus))

// 4
func startUpdatingPlaybackStatus() {
    displayLink.add(to: .main, forMode: .common)
}

func stopUpdatingPlaybackStatus() {
    displayLink.invalidate()
}

// 5
@objc
func updatePlaybackStatus() {
    let playbackProgress = Float(player.currentTime / player.duration)

    slider.setValue(playbackProgress, animated: true)
}

// 6
@objc
func didBeginDraggingSlider() {
    displayLink.isPaused = true
}

@objc
func didEndDraggingSlider() {
    let newPosition = player.duration * Double(slider.value)
    player.currentTime = newPosition

    displayLink.isPaused = false
}
  1. An outlet to the UISlider. Make sure that its min and max values are set to 0 and 1 respectively and add actions for its touchDown and valueChanged events to get notified when the user begins and ends dragging the slider respectively. Also, set one's isContinuous property to false, so the valueChanged event is reported only when the user releases the slider. This setup can also be done with Interface Builder and IBActions;
  2. A reference to the AVAudioPlayer, just for clarity;
  3. A property storing an instance of CADisplayLink. Since we use self as the target, the initialization of CADisplayLink should be done after initializing the parent class to be able to correctly reference it. For that purpose I made it lazy;
  4. Methods that start and stop updating the UI. You should call them when the playback status changes.
  5. The method that gets called during screen refresh, the logic for updating the UI is stored here.
  6. The methods for handling slider's events with fairly straightforward logic - pause display link when the user begins dragging the slider, so its value isn't updated during user interaction; seek the audio to the selected position and resume display link when the user released the slider.

Source - my blog post

Upvotes: 2

Akash More
Akash More

Reputation: 141

Tested code:-

Add timer variable

var timer: Timer?

in a function where you start playing your audio:

    slidderrr.value = 0.0
    slidderrr.maximumValue = Float((player?.duration)!)
    audioPlayer.play()
    timer = Timer.scheduledTimer(timeInterval: 0.0001, target: self, selector: #selector(self.updateSlider), userInfo: nil, repeats: true)

Don't forget to invalidate timer when you don't want to update slider

func stopPlayer() {
    player?.stop()
    timer?.invalidate()
    print("Player and timer stopped")
  }

Upvotes: 3

matt
matt

Reputation: 1

Have you tried func setValue(Float, animated: Bool) Sets the slider’s current value, allowing you to animate the change visually.

So something like slider.setValue(index, animated: true)

can be found here

https://developer.apple.com/documentation/uikit/uislider

let me know if that works!

Upvotes: 0

Related Questions