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
}
}
}