Debugprint mysteriously causes notifications to fire

36 Views Asked by At

MRE

struct ContentView: View {
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("SetupMusic").onTapGesture {
                setup()
            }
        }
        .padding()
    }
    
    
    private func setup() {
        debugPrint("Requesting access")
        MPMediaLibrary.requestAuthorization { permission in
            debugPrint("Setting up music player")
            let musicPlayer = MPMusicPlayerController.systemMusicPlayer
            musicPlayer.beginGeneratingPlaybackNotifications()
            debugPrint("Initial State: \(musicPlayer.playbackState)")  //MAGIC LINE!
            NotificationCenter.default.addObserver(forName: .MPMusicPlayerControllerPlaybackStateDidChange, object: musicPlayer, queue: .main) {  notification in
                debugPrint("State changed to \(musicPlayer.playbackState)")
            }
        }
    }
}

Add "NSAppleMusicUsageDescription" to your .plist file with string value which describes why you need access to Media player framework.

  1. Run app on real iOS device. Simulator does not support MPMediaPlayer framework.
  2. Hit button to allow for permission.
  3. Once granted you should get initial state. (Stopped/Paused)
  4. Go to system music app and play something on radio.
  5. Launch app again you should see state change to Playing
  6. Swipe down for notifications and pause music and state will change to Paused

If the magic line is removed, notifications will not fire. Why does it work like that?


EDIT:

I have now also tried the following setup code. And it still fails without the magic line

private func setup() {
        debugPrint("Requesting access")
        MPMediaLibrary.requestAuthorization { permission in
            DispatchQueue.main.async {
                debugPrint("Setting up music player")
                let musicPlayer = MPMusicPlayerController.systemMusicPlayer
                
                debugPrint("Initial State: \(musicPlayer.playbackState)") //MAGIC!
                NotificationCenter.default.addObserver(forName: .MPMusicPlayerControllerPlaybackStateDidChange, object: musicPlayer, queue: .main) {  notification in
                    debugPrint("State changed to \(musicPlayer.playbackState)")
                }
                
                musicPlayer.beginGeneratingPlaybackNotifications()
            }
        }
    }
1

There are 1 best solutions below

1
Rob Napier On

Do not call beginGeneratingPlaybackNotifications before you've called addObserver. You need to start observations first or you create race conditions where the notifications come before you start observing for them.

Generally you should call addObserver as early as possible, and be sure to call removeObserver in a balanced way. Otherwise you can get duplicates. To do this correctly, it's much easier if you create a separate ViewModel object to handle it. It's harder to maintain the notification unsubscribe token if you try to do this inside of the View. But in any case, addObserver needs to be first.

I would not bet on the callback from requestAuthorization being on the main queue. It doesn't promise that. debugPrint blocks on its output stream, however, so introducing it is going to have a big impacts on race conditions, like the one you've set up here.