How to know when MPMusicPlayerController changes playing item naturally - ios

I am using the MPMusicPlayerController to create a music player in my app. I have got it all working great except for one small issue:
When the songs changes naturally - one song finishes and the next starts from the set queue - the notification MPMusicPlayerControllerNowPlayingItemDidChange doesn't appear to be called.
At the moment I am utilising both the MPMusicPlayerControllerNowPlayingItemDidChange and MPMusicPlayerControllerPlaybackStateDidChange notifications. These cover playing, pausing, shuffle, repeat, next, previous etc. When the notifications are hit I then refresh the screen based on the MPMusicPlayerController to show the new song, artist or different button icons required. Neither of these are called though when a song finishes and the next one automatically starts playing - this means that the title and artist of the previous song is left until the user reloads the screen or interacts with the audio controls which is not good user experience.
Short of regularly checking whether the current name matches the playing name I don't know how to update this in the normal flow of the app.
NotificationCenter.default.addObserver(
forName: NSNotification.Name.MPMusicPlayerControllerNowPlayingItemDidChange,
object: musicPlayerController,
queue: nil) { _ in
// Update view
}

The answer to this turns out to be very simple but also difficult to spot if you're not looking in the right place.
Before we add our observers we need to begin generating the playback notifications:
musicPlayerController.beginGeneratingPlaybackNotifications()
NotificationCenter.default.addObserver(self,
selector: #selector(refreshView),
name: .MPMusicPlayerControllerPlaybackStateDidChange,
object: musicPlayerController)
NotificationCenter.default.addObserver(self,
selector: #selector(refreshView),
name: .MPMusicPlayerControllerNowPlayingItemDidChange,
object: musicPlayerController)
We also need to remember to end generating them when we leave (deallocate) the view:
deinit {
NotificationCenter.default.removeObserver(self, name: .MPMusicPlayerControllerPlaybackStateDidChange, object: nil)
NotificationCenter.default.removeObserver(self, name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: nil)
musicPlayerController.endGeneratingPlaybackNotifications()
}
The confusion came from the musicMediaPlayer returning a number of notifications even without this which didn't point to the fact we weren't observing all the notifications that were being fired.
Note: It is worth noting that as of the time of writing this it was in discussion whether there was a need to manually remove observers - I have included it here for answer completeness.

Related

Handle IOS app open from tapping on Now Playing

I'm using SwiftAudio to play audio, I want to detect when audio playing in background mode and app become active from tapping on Now Playing.
NotificationCenter
.default
.addObserver(self, selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
#objc func applicationDidBecomeActive() {
print("applicationDidBecomeActive")
}
right now I'm trying this way but it trigger applicationDidBecomeActive even when user open app from icon.
If there any other ways to know when user tapping on Now Playing, please let me know.
I'm very new to IOS development and Swift
Edit: I guess I can detect how app is entered foreground from NowPlaying by passing the expected objectsender to addObserver instead of passing nil object. I tried many times but still don't know what sender object it it.
You should look into MPNowPlayingInfoCenter
When your app is active again, you can check the nowPlayingInfo and playbackState to customise what you actually want to do in your app.
What is the difference between launching from Notification or Now Playing in your case? Maybe clarifying this a bit more will allow us to craft a better solution to your problem.

IOS Monitor audio route changes in background

Is it possible to monitor changes to the Audio route without playing music? I have found this question, but it seems to rely on audio being played.
My current setup looks something like this:
NotificationCenter.default.addObserver(self, selector: #selector(routeChange), name: .AVAudioSessionRouteChange, object: nil)
#objc
func routeChange(n: Notification) {
...
}
The background mode "Audio, AirPlay and Picture in Picture" is enabled.
My setup seems to work fine as long as the app is in foreground. As soon as it is in the background, routeChange will not be called anymore.

Block screen recoding but allow HDMI screen mirroring Swift 4

I need to stop Screen recoding, however I need to allow video sharing through HDMI.
I know the captured notification is usually used for this, but I cant find a way to separate out these two things.
UIScreen.main.addObserver(self, forKeyPath: "captured", options: .new, context: nil)
The above triggers notifications when screen recording is started / stopped. But also triggers when an external display is connected / disconnected (though I'm finding it to be intermittent here).
I have tried using the following to detect if a screen within the UIScreen is mirrored or just to check the screen count. Recordings show only one screen in the count, and mirrored screens should show a count of 2
recordingLabel.text = "count \(UIScreen.screens.count)"
for screen in UIScreen.screens {
if screen.mirrored != nil {
recordingLabel.text = "Mirrored - count \(UIScreen.screens.count)"
}
}
But again this is intermittent. Most of the time the count does not change on connecting a HDMI screen, but does change to 2 on disconnecting.
I have also found using UIScreen notifications work for connected / disconnected. But there seems to be a race condition happening with the captured notification that I still need to handle recordings.
NotificationCenter.default.addObserver(self, selector: #selector(connected), name: UIScreen.didConnectNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(disconnected), name: UIScreen.didDisconnectNotification, object: nil)
Is there any way to detect the type of recording or prevent the race from happening when both types of notifications trigger?
EDIT :
I have since found
NotificationCenter.default.addObserver(self, selector: #selector(captureChanged(notification:)), name: UIScreen.capturedDidChangeNotification, object: nil)
Which works better with the connection notifications, but it still triggers before I get a connected notification. I don't really want to add a timer to detect if the didConnect notification triggers after the captureChange, but that might be the only way

How to prevent SpriteKit game from crashing when Notification Center is opened

I am having a weird issue with a SpriteKit game. The app crashes when a user opens Notification center or Control Center. I want the worldNode layer to be paused and the menuNode layer to be presented when the applicationWillResignActive is called, which is working fine when the home button is pressed. I've tried pausing my game from within the AppDelegate functions applicationWillResignActive and I've tried pausing when applicationDidBecomeActive is called if the game has started. I've tried using the NotificationCenter approach from within the ViewController both work when the Home Button/Home Gesture is used. How Should I handle this? Or is this just a bug in iOS 11?
I have this in the viewDidLoad method of my ViewController
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
Then each one of those selectors
It crashes on both if trying to present the menu when the appDidBecomeActive and appWillResignActive
#objc func appDidBecomeActive() {
print("appDidBecomeActive")
if GameScene.sharedInstance != nil {
print("presentingMenu")
GameScene.sharedInstance.presentMenuNode()
}
}
#objc func appWillResignActive() {
print("appWillResignActive")
if GameScene.sharedInstance != nil {
print("presentingMenu")
GameScene.sharedInstance.presentMenuNode()
}
}
I feel like I may be trying to approach this the wrong way, but what I don't understand is why does it work when the Home button/Home gesture is fired?
Edit:
After more testing I found that everything works as expected when running on iOS 10 devices. However when I run the DemoBots app that apple provides from their sample code it doesn't crash on iOS 11 and basically does what I want to do, so there has got to be a better way to handle the transitions of the game state, any input is appreciated.

Keeping timers running when app enters background

I am making an app that relies on a timer that fires every minute to change a label from "x minutes left" to "(x-1) minutes left. Basically, it decrements the number every minute. I need the timer to function even when I close the app (not entirely, just press the home screen and leave it running in the background), so that when the user comes back to the app after leaving it in the background for 5 minutes, the label will say "(x-5) minutes left".
Right now, when I run it in the simulator it works perfectly, but when I run it on my phone it does not work. If I have the app open on my phone, it works, but if the app is running in the background, the label never decrements. Could this be due to differences in the way the simulator and actual iPhone handle multitasking? If so, how can I change my code so that the timer will still update the label every minute so that the correct number is displayed when the user reopens the app?
Here is my timer setup:
var individualTaskTimer = NSTimer()
func createTimerForTopTask(){
individualTaskTimer = NSTimer.scheduledTimerWithTimeInterval(tasks[0].minutes * 60, target: self, selector: "deleteTopTask", userInfo: nil, repeats: true)
}
As mentioned in Ewan Mellor's answer, you will not be able to rely on a timer while the app is in the background. So you will need to adjust as necessary when your app returns to the foreground.
Upon first reading the documentation, it might seem like viewWillAppear and viewWillDisappear (or viewDidDisappear) are the correct places to handle this. However, they do not get called when the app moves to/from the background.
Instead, you can make use of two notifications, UIApplicationWillResignActiveNotification and UIApplicationDidBecomeActiveNotification. The first notification will be sent to your app when it is about to go into the background. The second notification will be sent to your app when it is about to return to the foreground.
So in viewWillAppear you can register for the notifications as follows:
override func viewWillAppear(animated: Bool) {
// some other code
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: "UIApplicationDidBecomeActiveNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "goingAway", name: "UIApplicationWillResignActiveNotification", object: nil)
}
where activeAgain and goingAway are two functions you've written to enable and disable the timer. So based on the code snippet in your question, they would look something like this:
func activeAgain() {
let newTime = // calculate how much time is left (in seconds)
individualTaskTimer = NSTimer.scheduledTimerWithTimeInterval(newTime, target: self, selector: "deleteTopTask", userInfo: nil, repeats: true)
}
func goingAway() {
individualTaskTimer.invalidate()
}
Note that you need to unregister for the notifications when you switch away from this view. Doing this in viewWillDisappear is probably a good spot.
You can't do this. iOS will suspend your app after a short while when it goes into the background. Just update your label with the correct time when the app comes back to the foreground.

Resources