Search code examples
iosswiftuislideravfoundationavaudioplayer

Updating slider value in real time AVAudioPlayer SwiftUI


So I have a class called audio settings that is as follows:

class audioSettings: ObservableObject {
    
    var audioPlayer: AVAudioPlayer?
    var playing = false
    var playValue: TimeInterval = 0.0
    var playerDuration: TimeInterval = 146
    var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    
    
    func playSound(sound: String, type: String) {
        if let path = Bundle.main.path(forResource: sound, ofType: type) {
            do {
                if playing == false {
                    if (audioPlayer == nil) {
                        
                        
                        audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
                        audioPlayer?.prepareToPlay()
                        
                        audioPlayer?.play()
                        playing = true
                    }
                    
                }
                if playing == false {
                    
                    audioPlayer?.play()
                    playing = true
                }
                
                
            } catch {
                print("Could not find and play the sound file.")
            }
        }
        
    }
    
    
    
    func stopSound() {
        //   if playing == true {
        audioPlayer?.stop()
        audioPlayer = nil
        playing = false
        playValue = 0.0
        
        
        //   }
    }
    
    func pauseSound() {
        if playing == true {
            audioPlayer?.pause()
            playing = false
            
            
        }
    }
    
    
    func changeSliderValue() {
        if playing == true {
            pauseSound()
            audioPlayer?.currentTime = playValue
            
        }
        
        if playing == false {
            
            
            audioPlayer?.play()
            playing = true
        }
    }
}

And when I implement the code in to the view I have success with playing the file. I have success with stopping it and I have success with moving the slider and getting a new position in the audio.

Where i'm struggling is in getting the slider to update in real time as the audio is playing. If I hit play the slider will do nothing until I hit pause. At which point it will move to the correct position.

Here is the views code:

struct myExperienceFearChunk: View {
    
    
    @ObservedObject var audiosettings = audioSettings()
    
    
    var body: some View {
        
            
            HStack {
                Button(action: {
                    if (self.playButton == Image(systemName: "play.circle")) {
                        print("All Done")
                        self.audiosettings.playSound(sound: "audio file", type: "mp3")
                        self.audiosettings.timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
                        self.playButton = Image(systemName: "pause.circle")
                        
                    } else {
                        
                        self.audiosettings.pauseSound()
                        self.playButton = Image(systemName: "play.circle")
                        
                        
                    }

                    
                }) {
                    self.playButton
                        .foregroundColor(Color.white)
                        .font(.system(size: 44))
                }
                Button(action: {
                    print("All Done")
                    self.audiosettings.stopSound()
                    self.playButton = Image(systemName: "play.circle")
                    self.audiosettings.playValue = 0.0
                    
                }) {
                    self.stopButton
                        .foregroundColor(Color.white)
                        .font(.system(size: 44))
                    
                }
            }
            Slider(value: $audiosettings.playValue, in: TimeInterval(0.0)...audiosettings.playerDuration, onEditingChanged: { _ in
                self.audiosettings.changeSliderValue()
            })
                .onReceive(audiosettings.timer) { _ in
                    
                    if self.audiosettings.playing {
                        if let currentTime = self.audiosettings.audioPlayer?.currentTime {
                            self.audiosettings.playValue = currentTime
                            
                            if currentTime == TimeInterval(0.0) {
                                self.audiosettings.playing = false
                            }
                        }
                        
                    }
                    else {
                        self.audiosettings.playing = false
                        self.audiosettings.timer.upstream.connect().cancel()
                    }
            }
        }
    }
}

I know that the .onReceive is where the slider is being updated from when I hit pause, as without it the slider doesn't update at all unless dragged.

Any ideas how I can get the slider to update as the audio is playing setting the playValue as it goes.

Thanks.


Solution

  • Here is fixed code. Tested with Xcode 11.4 / iOS 13.4

    Main thing around fixes is to made @Published var playValue published to give chance for observed object in view be notified about changes in audioSettings and thus update Slider... others are minor.

    class audioSettings: ObservableObject {
        
        var audioPlayer: AVAudioPlayer?
        var playing = false
        @Published var playValue: TimeInterval = 0.0
        var playerDuration: TimeInterval = 146
        var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
        
        
        func playSound(sound: String, type: String) {
            if let path = Bundle.main.path(forResource: sound, ofType: type) {
                do {
                    if playing == false {
                        if (audioPlayer == nil) {
                            
                            
                            audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
                            audioPlayer?.prepareToPlay()
                            
                            audioPlayer?.play()
                            playing = true
                        }
                        
                    }
                    if playing == false {
                        
                        audioPlayer?.play()
                        playing = true
                    }
                    
                    
                } catch {
                    print("Could not find and play the sound file.")
                }
            }
            
        }
    
        func stopSound() {
            //   if playing == true {
            audioPlayer?.stop()
            audioPlayer = nil
            playing = false
            playValue = 0.0
            //   }
        }
        
        func pauseSound() {
            if playing == true {
                audioPlayer?.pause()
                playing = false
            }
        }
        
        func changeSliderValue() {
            if playing == true {
                pauseSound()
                audioPlayer?.currentTime = playValue
                
            }
            
            if playing == false {
                audioPlayer?.play()
                playing = true
            }
        }
    }
    
    struct myExperienceFearChunk: View {
        @ObservedObject var audiosettings = audioSettings()
        @State private var playButton: Image = Image(systemName: "play.circle")
        
        var body: some View {
            
            VStack {
            HStack {
                Button(action: {
                    if (self.playButton == Image(systemName: "play.circle")) {
                        print("All Done")
                        self.audiosettings.playSound(sound: "filename", type: "wav")
                        self.audiosettings.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
                        self.playButton = Image(systemName: "pause.circle")
                        
                    } else {
                        
                        self.audiosettings.pauseSound()
                        self.playButton = Image(systemName: "play.circle")
                    }
                }) {
                    self.playButton
                        .foregroundColor(Color.white)
                        .font(.system(size: 44))
                }
                Button(action: {
                    print("All Done")
                    self.audiosettings.stopSound()
                    self.playButton = Image(systemName: "play.circle")
                    self.audiosettings.playValue = 0.0
                    
                }) {
                    Image(systemName: "stop.circle")
                        .foregroundColor(Color.white)
                        .font(.system(size: 44))
                }
            }
            Slider(value: $audiosettings.playValue, in: TimeInterval(0.0)...audiosettings.playerDuration, onEditingChanged: { _ in
                self.audiosettings.changeSliderValue()
            })
                .onReceive(audiosettings.timer) { _ in
                    
                    if self.audiosettings.playing {
                        if let currentTime = self.audiosettings.audioPlayer?.currentTime {
                            self.audiosettings.playValue = currentTime
                            
                            if currentTime == TimeInterval(0.0) {
                                self.audiosettings.playing = false
                            }
                        }
                        
                    }
                    else {
                        self.audiosettings.playing = false
                        self.audiosettings.timer.upstream.connect().cancel()
                    }
                }
            }
        }
    }