SwiftUI AVAudioPlayer Lock Screen Control Not Resuming Slideshow on Unlock

63 Views Asked by At

Problem Details: I am developing an iOS app using SwiftUI that includes audio playback using AVAudioPlayer. In the app, I have a slideshow feature that should pause when the app goes into the background or is locked (i.e., when the screen is turned off) and resume when the app is brought back to the foreground or unlocked.

I have implemented lock screen audio control functionality using MPRemoteCommandCenter to allow users to play/pause audio from the lock screen and control center. However, I am facing an issue where the slideshow does not automatically resume when the user unlocks the screen after pausing the audio from the lock screen.

Expected Behavior:

  • When the app is active and playing audio, the slideshow should progress automatically.

  • When the app is paused (either manually or via the lock screen control), the slideshow should also pause.

  • Upon unlocking the screen, the slideshow should automatically resume.

Current Behavior:

  • The app correctly pauses the slideshow when the audio is paused from the lock screen.

  • However, when the user unlocks the screen, the slideshow remains paused and does not automatically resume as expected.

 func updateNowPlayingInfo(title: String) {
        let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
        nowPlayingInfoCenter.nowPlayingInfo = [
            MPMediaItemPropertyTitle: title,
            MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer?.currentTime ?? 0,
            MPMediaItemPropertyPlaybackDuration: audioPlayer?.duration ?? 0,
            MPNowPlayingInfoPropertyPlaybackRate: 1.0
        ]
    }

    func playAudio(_ index: Int) {
        audioPlayer?.stop()
        
        if isPlaying && !showFourImages && isMuted {
            print("Audio is muted in slideshow mode. Playback skipped.")
            return
        }
        
        guard index < currentImageNames.count else {
            print("Invalid index for audio playback: \(index)")
            return
        }
        
        let audioName = "voice\(index + 1)"
        
        guard let url = Bundle.main.url(forResource: audioName, withExtension: "mp3") else {
            print("Audio file not found for \(audioName)")
            return
        }
        
        do {
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
            
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer?.play()
            
            // Updating the NowPlayingInfoCenter with the audio details
            let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
            nowPlayingInfoCenter.nowPlayingInfo = [
                MPMediaItemPropertyTitle: "Audio Title for \(audioName)",
                MPMediaItemPropertyArtist: "Your Artist Name",
                MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer?.currentTime ?? 0,
                MPMediaItemPropertyPlaybackDuration: audioPlayer?.duration ?? 0,
                MPNowPlayingInfoPropertyPlaybackRate: 1.0
            ]
            
            print("Audio playing for: \(audioName)")
        } catch let error {
            print("Error playing audio for \(audioName): \(error.localizedDescription)")
        }
    }



    func timerInterval() -> Double {
        if showFourImages {
            return ContentView.fourImagesIntervals[speedFactor] ?? 0
        } else {
            return ContentView.oneImageIntervals[speedFactor] ?? 0
        }    }
    
    func startTimer() {
        timer?.invalidate()  // Stop any existing timer
        
        timer = Timer.scheduledTimer(withTimeInterval: timerInterval(), repeats: true) { _ in
            // Before starting the next audio, check if we should continue playback
            guard self.shouldContinuePlayback else {
                self.timer?.invalidate()  // Stop the timer if we shouldn't continue playback
                return
            }

            // Ensure we're in single image mode and audio is not muted
            if !self.showFourImages && !self.isMuted {
                // Play the audio for the current index
                self.playAudio(self.currentIndex)
                
                // After the audio finishes, update the current index and show the next image
                DispatchQueue.main.asyncAfter(deadline: .now() + (self.audioPlayer?.duration ?? self.timerInterval())) {
                    self.currentIndex += 1
                    
                    // If we're past the last image, loop back to the start
                    if self.currentIndex >= self.currentImageNames.count {
                        self.currentIndex = 0
                    }
                }
            } else {
                // If we're in the four image mode or audio is muted, just update the index after the interval
                DispatchQueue.main.asyncAfter(deadline: .now() + self.timerInterval()) {
                    self.currentIndex += self.showFourImages ? 4 : 1
                    
                    if self.currentIndex >= self.currentImageNames.count {
                        self.currentIndex = 0
                    }
                }
            }
        }
    }

***************************

 .onAppear {
            self.setupAudioSessionInterruptionObserver()
            self.setupRemoteTransportControls() // Adding the Remote Command Center setup
        }
        .onDisappear {
            NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
        }

    }
    
    func setupAudioSessionInterruptionObserver() {
        NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: nil, queue: .main) { (notification) in
            guard let userInfo = notification.userInfo,
                let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
                let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                    return
            }
            
            if type == .began {
                // Interruption began, take appropriate actions (like pausing audio)
            } else if type == .ended {
                guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption Ended - playback should resume
                    playAudio(currentIndex)
                }
            }
        }
    }
    
    // Add the new function below your existing functions

    func setupRemoteTransportControls() {
        let commandCenter = MPRemoteCommandCenter.shared()

        commandCenter.playCommand.addTarget { event in
            if let audioPlayer = self.audioPlayer, !audioPlayer.isPlaying {
                audioPlayer.play()
                self.isPlaying = true  // <-- Add this line
                self.shouldContinuePlayback = true
                return .success
            }
            return .commandFailed
        }

        commandCenter.pauseCommand.addTarget { event in
            if let audioPlayer = self.audioPlayer, audioPlayer.isPlaying {
                audioPlayer.pause()
                self.isPlaying = false  // <-- Add this line
                self.shouldContinuePlayback = false
                return .success
            }
            return .commandFailed
        }

        // Handle other commands as necessary...
    }
    func buttonTitle() -> String {
        switch language {
        case "arabic": return "bn"
        case "bangla": return "en"
        default: return "ar"
        }
    }
    
    func toggleLanguage() {
        switch language {
        case "arabic":
            language = "bangla"
        case "bangla":
            language = "english"
        default:
            language = "arabic"
        }
    }
    
}

struct FocusedTextField: UIViewRepresentable {
    @Binding var text: String
    var isFirstResponder: Bool = false
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.delegate = context.coordinator
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
        if isFirstResponder {
            uiView.becomeFirstResponder()
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UITextFieldDelegate {
        var parent: FocusedTextField
        
        init(_ parent: FocusedTextField) {
            self.parent = parent
        }
        
        func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            if let currentText = textField.text,
               let range = Range(range, in: currentText) {
                parent.text = currentText.replacingCharacters(in: range, with: string)
            }
            return true
        }
    }
}
0

There are 0 best solutions below