The main view of my app is a list of songs.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List (fileURLs, id: \.path) { song in
NavigationLink(destination: PlayView(songURL: song)) {
SongRow(songURL: song)
}
}
.padding()
.navigationBarTitle(Text("Songs"))
.navigationBarItems(trailing: addButton)
}
}
}
You tap on a song and it navigates to a PlayView, which instantiates an audio player:
import SwiftUI
import AVKit
struct PlayView: View {
var songURL: URL
@State var player : AVAudioPlayer!
var body: some View {
VStack {
Button(action: {
self.playPause()
}) {
Text("Play/Pause")
}
}.onAppear {
self.player = try! AVAudioPlayer(contentsOf: self.songURL)
self.playPause()
}
}
func playPause() {
if self.player.isPlaying {
self.player.pause()
} else {
self.player.play()
}
}
}
The problem is when I navigate to another song's PlayView, it plays the audio on top of the first song (i.e. both play simultaneously). How should I kill the first song when the second song plays?
In my project I'm using only one instance of final class
everywhere in the app. I think the problem in you case is that you're creating lots of instances of AVPlayer
in children views (PlayView
) but can control only one of them. Here is example of my solution:
import AVKit
import Combine
final class AudioPlayer: AVPlayer, ObservableObject {
@Published var currentPlaylist: [Song]? = nil
@Published var currentSong: Song? = nil
// ...
// MARK: player controls
func playPausePlayer(withSong song: Song, forPlaylist playlist: [Song]? = nil) {
setCurrentPlaylist(newPlaylist: playlist, newSong: song)
if currentSong == song {
continueOrPauseCurrentSong()
} else {
changeSong(with: song)
}
self.controlPanel?.updateNowPlaying()
}
// simplified func (I leave only what you need)
private func changeSong(with newSong: Song) {
if let url = URL.init(string: newSong.url) {
let nextPlayerItem = AVPlayerItem.init(url: url)
self.replaceCurrentItem(with: nextPlayerItem) // here you change the song
self.play()
currentSong = newSong
} else {...}
}
// ...
}
where Song
is just a struct with song data, like:
struct Song: Codable, Equatable, Identifiable, Hashable {
let id: String
let url: String
// and so on
}
At the SceneDelegate
func I set this AudioPlayer as an environmentObject
for using it in HomeView() and all it's children views. So you can play/pause your player or change the song (with .replaceCurrentItem(with: ...
method) from everywhere. In your case it should be something like this:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// ...
let homeView = ContentView()
.environment(\.managedObjectContext, context)
.environmentObject(AudioPlayer()) // here I set only one player for main window, so all the children will use it too
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: homeView)
self.window = window
window.makeKeyAndVisible()
}
}
}
// Based on your code snippet:
struct ContentView: View {
@EnvironmentObject var player: AudioPlayer // add this for main view
var body: some View {
NavigationView {
List (fileURLs, id: \.path) { song in
NavigationLink(destination: PlayView(songURL: song)) {
SongRow(songURL: song)
}
}
.padding()
.navigationBarTitle(Text("Songs"))
.navigationBarItems(trailing: addButton)
}
}
}
struct PlayView: View {
@EnvironmentObject var player: AudioPlayer // and for child view (they all will have one object
var songURL: URL
var body: some View {
VStack {
Button(action: {
self.playPause()
}) {
Text("Play/Pause")
}
}.onAppear {
// set or change currentItem of player
}
}
func playPause() {
if self.player.isPlaying {
self.player.pause()
} else {
self.player.play()
}
}
}
P.S. if I am wrong with the assumption that several instances of AVPlayer
may exist please tell me in comments