Home > front end >  How to control AVPlayer playback in SwiftUI
How to control AVPlayer playback in SwiftUI

Time:09-06

I am trying to play music on my app and manage to play/stop the music from the app's settings.

First I am creating an ObservableObject class called MusicPlayer:

class MusicPlayer: ObservableObject {
    
    @Published var isPlaying = AppDefaults.shared.isMusicPlaying()
    @Published var music : AVAudioPlayer? = nil
    
    func playMusic() {
        guard let strFilePath = Bundle.main.path(forResource: "music", ofType: "mp3") else { return }
        do {
            music = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: strFilePath))
        } catch {
            print(error)
        }
        
        music?.volume = 0.60
        music?.numberOfLoops = -1
        
        if isPlaying {
            music?.play()
        } else {
            music?.stop()
        }
    }
}

and then play the music in main app file:

@main
struct AppName: App {
    
    @StateObject private var player = MusicPlayer()
    
    var body: some Scene {
        WindowGroup {
            ContentsView()
                .onAppear {
                    player.playMusic()
                }
        }
    }
}

and then trying to stop/play the music using toggle from settings:

struct SettingsView: View {
    
    @StateObject private var player = MusicPlayer()
    
    var body: some View {
        Toggle("Music", isOn: $player.isPlaying)
            .onChange(of: player.isPlaying, perform: { _ in
                AppDefaults.shared.setMusic(player.isPlaying)
                if player.isPlaying {
                    player.music?.stop()
                } else {
                    player.music?.play()
                }
            })
    }
}

now the problem is switching to on or off doesn't change the state of playing. How can I fix this issue?

CodePudding user response:

The issue here is you are initializing your Viewmodel twice. So you have 2 different sources of truth. So there are 2 different AVAudioPlayer.

Solution: Create one single instance in the top View and pass this on to the views that need this.

As you decided to omit how SettingsView correlate with the other Views I can only give a more general solution.

Let´s asume the SettingsView is used in AppName:

@StateObject private var player = MusicPlayer()

WindowGroup {
    ....(ContentView stuff)
    SettingsView()
        // pass the observableObject on to the SettingsView and its children
        .environmentObject(player)
}

Then in SettingsView:

struct SettingsView: View {
    // get the observableObject from the environment
    @EnvironmentObject private var player: MusicPlayer
    
    var body: some View {
        Toggle("Music", isOn: $player.isPlaying)
            .onChange(of: player.isPlaying, perform: { _ in
                AppDefaults.shared.setMusic(player.isPlaying)
                if player.isPlaying {
                    player.music?.stop()
                } else {
                    player.music?.play()
                }
            })
    }
}
  • Related