MPRemoteCommandCenter works in simulator but not on device - ios

Edited to explain why its not a duplicate:
I am trying to use MPMusicPlayerController.systemMusicPlayer() to control music playback from my app, but I want to disable the Command Center previous track button. I also want to override the default Command Center play and next track functions. The code should be simple:
This code is in ViewController.swift - viewDidLoad
let commandCenter = MPRemoteCommandCenter.sharedCommandCenter()
commandCenter.previousTrackCommand.enabled = false
commandCenter.previousTrackCommand.addTargetWithHandler({ (commandEvent: MPRemoteCommandEvent!) -> MPRemoteCommandHandlerStatus in
self.empty()
return MPRemoteCommandHandlerStatus.Success
})
//MPRemoteCommandCenter.sharedCommandCenter().previousTrackCommand.addTarget(self, action: "empty")
commandCenter.nextTrackCommand.enabled = true
commandCenter.nextTrackCommand.addTargetWithHandler { (commandEvent: MPRemoteCommandEvent!) -> MPRemoteCommandHandlerStatus in
self.gameOver()
return MPRemoteCommandHandlerStatus.Success
}
commandCenter.playCommand.enabled = true
commandCenter.playCommand.addTargetWithHandler { (commandEvent: MPRemoteCommandEvent!) -> MPRemoteCommandHandlerStatus in
self.playing()
return MPRemoteCommandHandlerStatus.Success
}
Also, in AppDelegate.swift - application
UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
And in the iOS simulator (both iPad and iPhone), it works correctly, as can be seen in the first screenshot (of the simulator).
However, when deploying the app to my iPad, none of the MPRemoteCommandCenter commands work at all, as can be seen in the second screenshot (of an actual device).
This is different from the "dupliate" question (How Do I Get Audio Controls on Lock Screen/Control Center from AVAudioPlayer in Swift) in the following ways:
I am not using AVAudioSession, I am using MPMusicPlayerController.systemMusicPlayer()
I have already called beginReceivingRemoteControlEvents, so that cant be the issue (unless I have somehow called it incorrectly, in which case, I would love an answer explaining how else it should be called).
Thank you.
,

Related

Alternative to AVSystemController_AudioVolumeNotificationParameter post iOS 15?

The approved KVO approach for responding to device volume level changes stops detecting volume button presses after min/max outputVolume is reached. I'd like to continue to receive those button press events after min/max, so I assume I need to try this solution, even if it's not supported by Apple. However, I'm very much an amateur iOS programmer so I could use a hint. Here's what I've been doing (using RxSwift):
NotificationCenter.default.rx.notification(Notification.Name(rawValue: "AVSystemController_AudioVolumeNotificationParameter"))
.subscribe(onNext: { [weak self] notification in
guard let my = self else { return }
my.volumeNotification.accept(notification.userInfo!["AVSystemController_AudioVolumeNotificationParameter"] as! Double)
})
.disposed(by: disposeBag)
Should I instead be subscribing to a Notification named "MPVolumeControllerDataSource_SystemVolumeDidChange"?
Thanks in advance!
Three cheers for open source, specifically: JPSVolumeButtonHandler. This component works like a champ, and uses the Apple-approved KVO technique. Be aware that this component sets AVAudioSession options to .mixWithOthers which prevents MPRemoteCommandCenter from receiving/handing any BlueTooth commands. So if you need BT (Swift 5):
let volumeButtonHandler = JPSVolumeButtonHandler(up: {
// handle up press
}, downBlock: {
// handle down press
})
volumeButtonHandler.sessionOptions = [] // allow remote BT
I also found that programmatically setting the device volume to 0.5 before initializing the button handler avoided occasional min/max barriers. If the device initial volume was close to the min or max, the handler would stop after a few button presses:
try AVAudioSession.sharedInstance().setActive(true, options: [])
MPVolumeView(frame: .zero).volumeSlider.value = 0.5

How to Hide MPCommandCenter manually?

I am currently working on one Podcast app, in which we are using MPCommandCenter to enhance user experience to initialise MPCommandCenter I am using following code
let remoteCommands = MPRemoteCommandCenter.shared()
remoteCommands.playCommand.addTarget { event in
// Perform play action
return .success
}
remoteCommands.pauseCommand.addTarget { event in
// Perform pause action
return .success
}
After using this code MPCommandCenter properly display remote controls. But we have functionality that allow users to Close currently playing podcast. And I am not able to figure out the way to unregister commandcenter manually. Can anyone help me to figure this one out.
remoteCommands.playCommand.removeTarget(self)
remoteCommands.pauseCommand.removeTarget(self)
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
try? AVAudioSession.sharedInstance().setActive(false)

Why doesn't my iOS (Swift) app properly recognize some external display devices?

So I have an odd issue and my google-fu utterly fails to even provide me the basis of where to start investigating, so even useful keywords to search on may be of use.
I have an iOS application written in swift. I have a model hooked up to receive notifications about external displays. On some adaptors, I'm able to properly detect and respond to the presence of an external display and programatically switch it out to be something other than a mirror (see code block below). But with another adaptor, instead of just 'magically' becoming a second screen, I'm asked to 'trust' the external device, and it simply mirrors the device screen. Not the intended design at all.
func addSecondScreen(screen: UIScreen){
self.externalWindow = UIWindow.init(frame: screen.bounds)
self.externalWindow!.screen = screen
self.externalWindow!.rootViewController = self.externalVC
self.externalWindow!.isHidden = false;
}
#objc func handleScreenDidConnectNotification( _ notification: NSNotification){
let newScreen = notification.object as! UIScreen
if(self.externalWindow == nil){
addSecondScreen(screen: newScreen)
}
}
#objc func handleScreenDidDisconnectNotification( _ notification: NSNotification){
if let externalWindow = self.externalWindow{
externalWindow.isHidden = true
self.externalWindow = nil
}
}
The worst issue here is that because I'm connecting to an external display to do this, I can't even run this code through the debugger to find out what is going on. I don't know where to even begin.
Any ideas?
Edit:
Thanks to someone pointing out wifi debugging, I can tell you my notifications are firing off, but they're both firing at the same time, one after the other, when the external adaptor is disconnected.

addUIInterruptionMonitor(withDescription:handler:) not working on iOS 10 or 9

The following tests works fine on iOS 11. It dismisses the alert asking permissions to use the locations services and then zooms in in the map. On iOS 10 or 9, it does none of this and the test still succeeds
func testExample() {
let app = XCUIApplication()
var handled = false
var appeared = false
let token = addUIInterruptionMonitor(withDescription: "Location") { (alert) -> Bool in
appeared = true
let allow = alert.buttons["Allow"]
if allow.exists {
allow.tap()
handled = true
return true
}
return false
}
// Interruption won't happen without some kind of action.
app.tap()
removeUIInterruptionMonitor(token)
XCTAssertTrue(appeared && handled)
}
Does anyone have an idea why and/or a workaround?
Here's a project where you can reproduce the issue: https://github.com/TitouanVanBelle/Map
Update
Xcode 9.3 Beta's Changelogs show the following
XCTest UI interruption monitors now work correctly on devices and simulators running iOS 10. (33278282)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons["Allow"]
if allowBtn.waitForExistence(timeout: 10) {
allowBtn.tap()
}
Update .exists to .waitForExistence(timeout: 10), detail please check comments.
I had this problem and River2202's solution worked for me.
Note that this is not a fix to get the UIInterruptionMonitor to work, but a different way of dismissing the alert. You may as well remove the addUIInterruptionMonitor setup. You'll need to have the springboard.buttons["Allow"].exists test anywhere the permission alert could appear. If possible, force it to appear at an early stage of the testing so you don't need to worry about it again later.
Happily the springboard.buttons["Allow"].exists code still works in iOS 11, so you can have a single code path and not have to do one thing for iOS 10 and another for iOS 11.
Incidentally, I logged the base issue (that addUIInterruptionMonitor is not working pre-iOS 11) as a bug with Apple. It has been closed as a duplicate now, so I guess they acknowledge that it is a bug.
I used the #River2202 solution and it works better than the interruption one.
If you decide to use that, I strongly suggest that you use a waiter function. I created this one in order to wait on any kind of XCUIElement to appear:
Try it!
// function to wait for an ui element to appear on screen, with a default wait time of 20 seconds
// XCTWaiter was introduced after Xcode 8.3, which is handling better the timewait, it's not failing the test. It uses an enum which returns: 'Waiters can be used with or without a delegate to respond to events such as completion, timeout, or invalid expectation fulfilment.'
#discardableResult
func uiElementExists(for element: XCUIElement, timeout: TimeInterval = 20) -> Bool {
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true"), object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
guard result == .completed else {
return false
}
return true
}

Audio playback lock screen controls not displaying on iPhone

I am testing this using iOS 10.2 on my actual iPhone 6s device.
I am playing streamed audio and am able to play/pause audio, skip tracks, etc. I also have enabled background modes and the audio plays in the background and continues through a playlist properly. The only issue I am having is getting the lock screen controls to show up. Nothing displays at all...
In viewDidLoad() of my MainViewController, right when my app launches, I call this...
func setupAudioSession(){
UIApplication.shared.beginReceivingRemoteControlEvents()
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
self.becomeFirstResponder()
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
and then in my AudioPlayer class after I begin playing audio I call ...
func setupLockScreen(){
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action:#selector(skipTrack))
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyTitle: "TESTING"]
}
When I lock my iPhone and then tap the power button again to go to the lock screen, the audio controls are not displayed at all. It is as if no audio is playing, I just see my normal background photo. Also no controls are displayed in the control panel (swiping up on home screen and then swiping left to where the music controls should be).
Is the issue because I am not using AVAudioPlayer or AVPlayer? But then how does, for example, Spotify get the lock screen controls to display using their own custom audio player? Thanks for any advice / help
The issue turned out to be this line...
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.duckOthers)
Once I changed it to
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: [])
everything worked fine. So it seems that passing in any argument for AVAudioSessionCategoryPlaybackOptions causes the lock screen controls to not display. I also tried passing in .mixWithOthers an that too caused the lock screen controls to not be displayed
In Swift 4. This example is only to show the player on the lock screen and works with iOS 11. To know how to play auidio on the device you can follow this thread https://stackoverflow.com/a/47710809/1283517
import MediaPlayer
import AVFoundation
Declare player
var player : AVPlayer?
Now create a function
func setupLockScreen(){
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(controlPause))
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyTitle: currentStation]
}
now create a function for control play and pause event. I have created a BOOL "isPlaying" to determine the status of the player.
#objc func controlPause() {
if isPlaying == true {
player?.pause()
isPlaying = false
} else {
player?.play()
isPlaying = true
}
}
And ready. Now the player will be displayed on the lock screen
Yes, for the lock screen to work you need to use iOS APIs to play audio. Not sure how Spotify does it but they may be using a second audio session in parallel for this purpose and use the controls to control both. Your background handler (the singleton in my case) could start playing the second audio with 0 volume when it goes into background and stop it when in foreground. I haven't tested it myself but an option to try.

Resources