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.
- Run app on real iOS device. Simulator does not support MPMediaPlayer framework.
- Hit button to allow for permission.
- Once granted you should get initial state. (Stopped/Paused)
- Go to system music app and play something on radio.
- Launch app again you should see state change to Playing
- 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()
}
}
}
Do not call
beginGeneratingPlaybackNotificationsbefore you've calledaddObserver. 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
addObserveras early as possible, and be sure to callremoveObserverin 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,addObserverneeds to be first.I would not bet on the callback from
requestAuthorizationbeing on the main queue. It doesn't promise that.debugPrintblocks 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.